Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4aacea2a3 | ||
|
|
8bbe6ebf75 | ||
|
|
a23d28f2cf | ||
|
|
f51e8859ee | ||
|
|
d235dee955 | ||
|
|
2afef4ea3d | ||
|
|
f709d6a566 | ||
|
|
532915b95d | ||
|
|
456e40bedf | ||
|
|
018b7e270f | ||
|
|
f8b8b4c97a | ||
|
|
978d7d0863 | ||
|
|
594a14d6ca | ||
|
|
c628aa9aef | ||
|
|
3060ebf79c | ||
|
|
1a34c7374b | ||
|
|
c262681011 | ||
|
|
f8ba5c46e4 | ||
|
|
b789250cb8 | ||
|
|
5df458739d | ||
|
|
d6fa9a2a03 | ||
|
|
ec15af01dd | ||
|
|
7073336f81 | ||
|
|
3d3380f27e | ||
|
|
c69998c7cb | ||
|
|
1663bfeac7 | ||
|
|
afc3bfb3b8 | ||
|
|
b6bfc25727 | ||
|
|
7fac0fe79f | ||
|
|
5967347b80 | ||
|
|
855a445c1a |
32
CHANGELOG.md
@@ -1,14 +1,38 @@
|
||||
# Changelog
|
||||
|
||||
## 0.6.2 (2020-08-16)
|
||||
|
||||
* 🐛 fixed disabling error reporting for november 2019 update | [commit](https://github.com/undergroundwires/privacy.sexy/commit/5967347b80976a519f6f4eb1972a62f3e600df2b)
|
||||
* 🐛 fixed blank screen and icons on mac | [commit](https://github.com/undergroundwires/privacy.sexy/commit/7fac0fe79f252e8f9dda4f6f83cd6fa4ba2b539f)
|
||||
* 🐛 fixed removing onedrive does not delete scheduled tasks | [commit](https://github.com/undergroundwires/privacy.sexy/commit/b6bfc2572740c0cd46d3bc0058fa767dd5fa862e)
|
||||
* ⚙️ enhanced tweak to disable for office telemetry | [commit](https://github.com/undergroundwires/privacy.sexy/commit/afc3bfb3b8896f332c9a196973ded3dce8fd21e4)
|
||||
* ✨ added script to clear dotnet telemery | [commit](https://github.com/undergroundwires/privacy.sexy/commit/1663bfeac7b6580b1335ca5fcf3587b69c080c72)
|
||||
* 🐛 fixed changing time server not working | [commit](https://github.com/undergroundwires/privacy.sexy/commit/c69998c7cb29ffcf40f0af03b73150736581da69)
|
||||
* 🔥 removed disabling ClickToRun as it breaks office | [commit](https://github.com/undergroundwires/privacy.sexy/commit/3d3380f27ebeea53f17f49974aaa89300ffaf2dd)
|
||||
|
||||
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.6.1...0.6.2)
|
||||
|
||||
## 0.6.1 (2020-08-09)
|
||||
|
||||
* updated documentation | [commit](https://github.com/undergroundwires/privacy.sexy/commit/5963d2bac551083f9d16cce6b851abf0e8b88ce7)
|
||||
* fixed typo in footer | [commit](https://github.com/undergroundwires/privacy.sexy/commit/5c15a7a64aaf24578a32713dec491bf494216303)
|
||||
* more scripts can be reverted | [commit](https://github.com/undergroundwires/privacy.sexy/commit/831c014f977515454ee6dc664d77a8c434495501)
|
||||
* moved windows connect now to security & recommended | [commit](https://github.com/undergroundwires/privacy.sexy/commit/6049a2b834d8d17af741f8d8f8b07cd15153b001)
|
||||
* fixed mac / linux download links | [commit](https://github.com/undergroundwires/privacy.sexy/commit/4c8be45e287b5ea009d6f828f7f327f37850569e)
|
||||
* tweaks to disable webcam, speech and compatibility telemetry | [commit](https://github.com/undergroundwires/privacy.sexy/commit/a5dbe66fc175e39397f296ab2ff703e9b0ab4d7c)
|
||||
* refactorings | [commit](https://github.com/undergroundwires/privacy.sexy/commit/66d4d39d5bf3db305450514c6b6224654dafbfb2)
|
||||
* fixed removing onedrive does not clean start menu / quick access | [commit](https://github.com/undergroundwires/privacy.sexy/commit/1cc12195a3e9a11c590d3ed64d80299b50f74838)
|
||||
|
||||
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.6.0...0.6.1)
|
||||
|
||||
## 0.6.0 (2020-07-26)
|
||||
|
||||
* fixed dead links in documentation | [commit](https://github.com/undergroundwires/privacy.sexy/commit/25ce236a7737decaf2eb9b8c29a4c4f34d43f770)
|
||||
* runs tests on each push on the repository | [commit](https://github.com/undergroundwires/privacy.sexy/commit/73c426844a0330718a9ab7de12b61ca05e853323)
|
||||
* code area now shows "how" before "why" | [commit](https://github.com/undergroundwires/privacy.sexy/commit/4ff4b52202b1c5dbfe2b80580bbe7d93132ab05c)
|
||||
* support for desktop versions #20 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/5a27f9d86c96be88bb52aa69a84a7e057ac072be)
|
||||
* reworked on footer & removed github icon | [commit](https://github.com/undergroundwires/privacy.sexy/commit/8ab062454db1d8504fb2df4fbb39a7002bafeb09)
|
||||
* updated dependencies to latest | [commit](https://github.com/undergroundwires/privacy.sexy/commit/c335ab33ff612a19af1278bdcc88a14779e8bb91)
|
||||
* updated documentation | [commit](https://github.com/undergroundwires/privacy.sexy/commit/81dfbbef7a8786751fa7693010c80561c5ac6bce)
|
||||
* support for desktop versions #20 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/04b9b59e14766ccd251474ad3710baf1f682fd49)
|
||||
* reworked on footer & removed github icon | [commit](https://github.com/undergroundwires/privacy.sexy/commit/60a5a2aa4026d384bef9e6a203f1b7514a269c33)
|
||||
* updated dependencies to latest | [commit](https://github.com/undergroundwires/privacy.sexy/commit/45816a2bccb3d11a50e3f2bc19c0a6cc2587deaa)
|
||||
|
||||
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.5.0...0.6.0)
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
- 🙏 DO
|
||||
- Document your changes in the pull request
|
||||
- ❗ DON'T
|
||||
- Do not update the versions, current version is only [set by the maintainer](./img/gitops.png) and updated automatically by [bump-everywhere](https://github.com/undergroundwires/bump-everywhere)
|
||||
- Do not update the versions, current version is only [set by the maintainer](./img/architecture/gitops.png) and updated automatically by [bump-everywhere](https://github.com/undergroundwires/bump-everywhere)
|
||||
|
||||
## Guidelines
|
||||
|
||||
|
||||
14
README.md
@@ -15,9 +15,9 @@
|
||||
## Get started
|
||||
|
||||
- Online version: [https://privacy.sexy](https://privacy.sexy)
|
||||
- or download latest desktop version for [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.6.0/privacy.sexy-Setup-0.6.0.exe), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.6.0/privacy.sexy-0.6.0.AppImage), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.6.0/privacy.sexy-0.6.0.dmg)
|
||||
- or download latest desktop version for [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.6.2/privacy.sexy-Setup-0.6.2.exe), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.6.2/privacy.sexy-0.6.2.AppImage), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.6.2/privacy.sexy-0.6.2.dmg)
|
||||
|
||||

|
||||
[](https://privacy.sexy)
|
||||
|
||||
## Why
|
||||
|
||||
@@ -48,8 +48,8 @@
|
||||
- Development: `npm run serve` to compile & hot-reload for development.
|
||||
- Production: `npm run build` to prepare files for distribution.
|
||||
- Or run using Docker:
|
||||
1. Build: `docker build -t undergroundwires/privacy.sexy:0.6.0 .`
|
||||
2. Run: `docker run -it -p 8080:80 --rm --name privacy.sexy-0.6.0 undergroundwires/privacy.sexy:0.6.0`
|
||||
1. Build: `docker build -t undergroundwires/privacy.sexy:0.6.2 .`
|
||||
2. Run: `docker run -it -p 8080:80 --rm --name privacy.sexy-0.6.2 undergroundwires/privacy.sexy:0.6.2`
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -68,11 +68,11 @@
|
||||
- The [state](src/application/State/ApplicationState.ts) is a mutable singleton & event producer.
|
||||
- The application is defined & controlled in a [single YAML file](src/application/application.yaml) (see [Data-driven programming](https://en.wikipedia.org/wiki/Data-driven_programming))
|
||||
|
||||

|
||||

|
||||
|
||||
### AWS Infrastructure
|
||||
|
||||
[](https://github.com/undergroundwires/aws-static-site-with-cd)
|
||||
[](https://github.com/undergroundwires/aws-static-site-with-cd)
|
||||
|
||||
- It uses infrastructure from the following repository: [aws-static-site-with-cd](https://github.com/undergroundwires/aws-static-site-with-cd)
|
||||
- Runs on AWS 100% serverless and automatically provisioned using [GitHub Actions](.github/workflows/).
|
||||
@@ -84,4 +84,4 @@
|
||||
- Versioning, tagging, creation of `CHANGELOG.md` and releasing is automated using [bump-everywhere](https://github.com/undergroundwires/bump-everywhere) action
|
||||
- Everything that's merged in the master goes directly to production.
|
||||
|
||||
[](.github/workflows/)
|
||||
[](.github/workflows/)
|
||||
|
||||
5
build/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# build
|
||||
|
||||
- These are the file that are used by electron.
|
||||
- Logos are created by from the [PNG icon](./../public/icon.png)
|
||||
- by running `npx electron-icon-builder --input=./public/icon.png --output=build --flatten`
|
||||
BIN
build/icons/1024x1024.png
Normal file
|
After Width: | Height: | Size: 225 KiB |
BIN
build/icons/128x128.png
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
BIN
build/icons/16x16.png
Normal file
|
After Width: | Height: | Size: 740 B |
BIN
build/icons/24x24.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
build/icons/256x256.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
build/icons/32x32.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
build/icons/48x48.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
build/icons/512x512.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
build/icons/64x64.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
build/icons/icon.icns
Normal file
BIN
build/icons/icon.ico
Normal file
|
After Width: | Height: | Size: 353 KiB |
BIN
img/app.png
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 483 KiB After Width: | Height: | Size: 483 KiB |
1291
package-lock.json
generated
27
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "privacy.sexy",
|
||||
"version": "0.6.0",
|
||||
"version": "0.6.2",
|
||||
"author": "undergroundwires",
|
||||
"description": "Enforce privacy & security best-practices on Windows, because privacy is sexy 🍑🍆",
|
||||
"private": true,
|
||||
@@ -30,8 +30,8 @@
|
||||
"file-saver": "^2.0.2",
|
||||
"inversify": "^5.0.1",
|
||||
"liquor-tree": "^0.2.70",
|
||||
"v-tooltip": "^2.0.2",
|
||||
"vue": "^2.6.11",
|
||||
"v-tooltip": "2.0.2",
|
||||
"vue": "^2.6.12",
|
||||
"vue-class-component": "^7.2.5",
|
||||
"vue-js-modal": "^2.0.0-rc.6",
|
||||
"vue-property-decorator": "^9.0.0"
|
||||
@@ -40,16 +40,15 @@
|
||||
"@types/ace": "0.0.43",
|
||||
"@types/chai": "^4.2.12",
|
||||
"@types/file-saver": "^2.0.1",
|
||||
"@types/mocha": "^8.0.0",
|
||||
"@types/node": "12.0.0",
|
||||
"@vue/cli-plugin-typescript": "^4.4.6",
|
||||
"@vue/cli-plugin-unit-mocha": "^4.4.6",
|
||||
"@vue/cli-service": "^4.4.6",
|
||||
"@vue/test-utils": "1.0.3",
|
||||
"@types/mocha": "^8.0.3",
|
||||
"@vue/cli-plugin-typescript": "^4.5.4",
|
||||
"@vue/cli-plugin-unit-mocha": "^4.5.4",
|
||||
"@vue/cli-service": "^4.5.4",
|
||||
"@vue/test-utils": "1.0.4",
|
||||
"chai": "^4.2.0",
|
||||
"electron": "^9.1.1",
|
||||
"electron": "^10.1.0",
|
||||
"electron-devtools-installer": "^3.1.1",
|
||||
"electron-log": "^4.2.2",
|
||||
"electron-log": "^4.2.4",
|
||||
"electron-updater": "^4.3.4",
|
||||
"js-yaml-loader": "^1.2.2",
|
||||
"markdownlint-cli": "^0.23.2",
|
||||
@@ -58,10 +57,10 @@
|
||||
"remark-preset-lint-consistent": "^3.0.1",
|
||||
"remark-validate-links": "^10.0.2",
|
||||
"sass": "^1.26.10",
|
||||
"sass-loader": "^9.0.2",
|
||||
"typescript": "^3.9.7",
|
||||
"sass-loader": "^10.0.1",
|
||||
"typescript": "^4.0.2",
|
||||
"vue-cli-plugin-electron-builder": "^2.0.0-rc.4",
|
||||
"vue-template-compiler": "^2.6.11",
|
||||
"vue-template-compiler": "^2.6.12",
|
||||
"yaml-lint": "^1.2.4"
|
||||
}
|
||||
}
|
||||
|
||||
BIN
public/icon.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
@@ -12,8 +12,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop } from 'vue-property-decorator';
|
||||
import { ApplicationState } from '@/application/State/ApplicationState';
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
import TheHeader from '@/presentation/TheHeader.vue';
|
||||
import TheFooter from '@/presentation/TheFooter/TheFooter.vue';
|
||||
import TheCodeArea from '@/presentation/TheCodeArea.vue';
|
||||
|
||||
@@ -11,6 +11,7 @@ import { Script } from '@/domain/Script';
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
import { IApplicationCode } from './Code/IApplicationCode';
|
||||
import applicationFile from 'js-yaml-loader!@/application/application.yaml';
|
||||
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
||||
|
||||
/** Mutatable singleton application state that's the single source of truth throughout the application */
|
||||
export class ApplicationState implements IApplicationState {
|
||||
@@ -37,7 +38,7 @@ export class ApplicationState implements IApplicationState {
|
||||
public readonly app: IApplication,
|
||||
/** Initially selected scripts */
|
||||
public readonly defaultScripts: Script[]) {
|
||||
this.selection = new UserSelection(app, defaultScripts);
|
||||
this.selection = new UserSelection(app, defaultScripts.map((script) => new SelectedScript(script, false)));
|
||||
this.code = new ApplicationCode(this.selection, app.version);
|
||||
this.filter = new UserFilter(app);
|
||||
}
|
||||
|
||||
@@ -1,30 +1,38 @@
|
||||
import { CodeChangedEvent } from './Event/CodeChangedEvent';
|
||||
import { CodePosition } from './Position/CodePosition';
|
||||
import { ICodeChangedEvent } from './Event/ICodeChangedEvent';
|
||||
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
||||
import { IUserSelection } from '@/application/State/Selection/IUserSelection';
|
||||
import { UserScriptGenerator } from './UserScriptGenerator';
|
||||
import { UserScriptGenerator } from './Generation/UserScriptGenerator';
|
||||
import { Signal } from '@/infrastructure/Events/Signal';
|
||||
import { IApplicationCode } from './IApplicationCode';
|
||||
import { IUserScriptGenerator } from './IUserScriptGenerator';
|
||||
import { IUserScriptGenerator } from './Generation/IUserScriptGenerator';
|
||||
|
||||
export class ApplicationCode implements IApplicationCode {
|
||||
public readonly changed = new Signal<string>();
|
||||
public readonly changed = new Signal<ICodeChangedEvent>();
|
||||
public current: string;
|
||||
|
||||
private readonly generator: IUserScriptGenerator = new UserScriptGenerator();
|
||||
private scriptPositions = new Map<SelectedScript, CodePosition>();
|
||||
|
||||
constructor(
|
||||
userSelection: IUserSelection,
|
||||
private readonly version: string) {
|
||||
private readonly version: string,
|
||||
private readonly generator: IUserScriptGenerator = new UserScriptGenerator()) {
|
||||
if (!userSelection) { throw new Error('userSelection is null or undefined'); }
|
||||
if (!version) { throw new Error('version is null or undefined'); }
|
||||
this.generator = new UserScriptGenerator();
|
||||
if (!generator) { throw new Error('generator is null or undefined'); }
|
||||
this.setCode(userSelection.selectedScripts);
|
||||
userSelection.changed.on((scripts) => {
|
||||
this.setCode(scripts);
|
||||
});
|
||||
}
|
||||
|
||||
private setCode(scripts: ReadonlyArray<SelectedScript>) {
|
||||
this.current = scripts.length === 0 ? '' : this.generator.buildCode(scripts, this.version);
|
||||
this.changed.notify(this.current);
|
||||
private setCode(scripts: ReadonlyArray<SelectedScript>): void {
|
||||
const oldScripts = Array.from(this.scriptPositions.keys());
|
||||
const code = this.generator.buildCode(scripts, this.version);
|
||||
this.current = code.code;
|
||||
this.scriptPositions = code.scriptPositions;
|
||||
const event = new CodeChangedEvent(code.code, oldScripts, code.scriptPositions);
|
||||
this.changed.notify(event);
|
||||
}
|
||||
}
|
||||
|
||||
64
src/application/State/Code/Event/CodeChangedEvent.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { ICodeChangedEvent } from './ICodeChangedEvent';
|
||||
import { SelectedScript } from '../../Selection/SelectedScript';
|
||||
import { IScript } from '@/domain/IScript';
|
||||
import { ICodePosition } from '@/application/State/Code/Position/ICodePosition';
|
||||
|
||||
export class CodeChangedEvent implements ICodeChangedEvent {
|
||||
public readonly code: string;
|
||||
public readonly addedScripts: ReadonlyArray<IScript>;
|
||||
public readonly removedScripts: ReadonlyArray<IScript>;
|
||||
public readonly changedScripts: ReadonlyArray<IScript>;
|
||||
|
||||
private readonly scripts: Map<IScript, ICodePosition>;
|
||||
|
||||
constructor(
|
||||
code: string,
|
||||
oldScripts: ReadonlyArray<SelectedScript>,
|
||||
scripts: Map<SelectedScript, ICodePosition>) {
|
||||
ensureAllPositionsExist(code, Array.from(scripts.values()));
|
||||
this.code = code;
|
||||
const newScripts = Array.from(scripts.keys());
|
||||
this.addedScripts = selectIfNotExists(newScripts, oldScripts);
|
||||
this.removedScripts = selectIfNotExists(oldScripts, newScripts);
|
||||
this.changedScripts = getChangedScripts(oldScripts, newScripts);
|
||||
this.scripts = new Map<IScript, ICodePosition>();
|
||||
scripts.forEach((position, selection) => {
|
||||
this.scripts.set(selection.script, position);
|
||||
});
|
||||
}
|
||||
|
||||
public isEmpty(): boolean {
|
||||
return this.scripts.size === 0;
|
||||
}
|
||||
|
||||
public getScriptPositionInCode(script: IScript): ICodePosition {
|
||||
return this.scripts.get(script);
|
||||
}
|
||||
}
|
||||
|
||||
function ensureAllPositionsExist(script: string, positions: ReadonlyArray<ICodePosition>) {
|
||||
const totalLines = script.split(/\r\n|\r|\n/).length;
|
||||
for (const position of positions) {
|
||||
if (position.endLine > totalLines) {
|
||||
throw new Error(`script end line (${position.endLine}) is out of range.` +
|
||||
`(total code lines: ${totalLines}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getChangedScripts(
|
||||
oldScripts: ReadonlyArray<SelectedScript>,
|
||||
newScripts: ReadonlyArray<SelectedScript>): ReadonlyArray<IScript> {
|
||||
return newScripts
|
||||
.filter((newScript) => oldScripts.find((oldScript) => oldScript.id === newScript.id
|
||||
&& oldScript.revert !== newScript.revert ))
|
||||
.map((selection) => selection.script);
|
||||
}
|
||||
|
||||
function selectIfNotExists(
|
||||
selectableContainer: ReadonlyArray<SelectedScript>,
|
||||
test: ReadonlyArray<SelectedScript>) {
|
||||
return selectableContainer
|
||||
.filter((script) => !test.find((oldScript) => oldScript.id === script.id))
|
||||
.map((selection) => selection.script);
|
||||
}
|
||||
11
src/application/State/Code/Event/ICodeChangedEvent.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { IScript } from '@/domain/IScript';
|
||||
import { ICodePosition } from '@/application/State/Code/Position/ICodePosition';
|
||||
|
||||
export interface ICodeChangedEvent {
|
||||
readonly code: string;
|
||||
addedScripts: ReadonlyArray<IScript>;
|
||||
removedScripts: ReadonlyArray<IScript>;
|
||||
changedScripts: ReadonlyArray<IScript>;
|
||||
isEmpty(): boolean;
|
||||
getScriptPositionInCode(script: IScript): ICodePosition;
|
||||
}
|
||||
@@ -4,8 +4,20 @@ const TotalFunctionSeparatorChars = 58;
|
||||
export class CodeBuilder {
|
||||
private readonly lines = new Array<string>();
|
||||
|
||||
// Returns current line starting from 0 (no lines), or 1 (have single line)
|
||||
public get currentLine(): number {
|
||||
return this.lines.length;
|
||||
}
|
||||
|
||||
public appendLine(code?: string): CodeBuilder {
|
||||
this.lines.push(code);
|
||||
if (!code) {
|
||||
this.lines.push('');
|
||||
return this;
|
||||
}
|
||||
const lines = code.match(/[^\r\n]+/g);
|
||||
for (const line of lines) {
|
||||
this.lines.push(line);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
7
src/application/State/Code/Generation/IUserScript.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
||||
import { ICodePosition } from '@/application/State/Code/Position/ICodePosition';
|
||||
|
||||
export interface IUserScript {
|
||||
code: string;
|
||||
scriptPositions: Map<SelectedScript, ICodePosition>;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
||||
import { IUserScript } from './IUserScript';
|
||||
export interface IUserScriptGenerator {
|
||||
buildCode(
|
||||
selectedScripts: ReadonlyArray<SelectedScript>,
|
||||
version: string): IUserScript;
|
||||
}
|
||||
68
src/application/State/Code/Generation/UserScriptGenerator.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
||||
import { IUserScriptGenerator } from './IUserScriptGenerator';
|
||||
import { CodeBuilder } from './CodeBuilder';
|
||||
import { ICodePosition } from '@/application/State/Code/Position/ICodePosition';
|
||||
import { CodePosition } from '../Position/CodePosition';
|
||||
import { IUserScript } from './IUserScript';
|
||||
|
||||
export const adminRightsScript = {
|
||||
name: 'Ensure admin privileges',
|
||||
code: 'fltmc >nul 2>&1 || (\n' +
|
||||
' echo Administrator privileges are required.\n' +
|
||||
' PowerShell Start -Verb RunAs \'%0\' 2> nul || (\n' +
|
||||
' echo Right-click on the script and select "Run as administrator".\n' +
|
||||
' pause & exit 1\n' +
|
||||
' )\n' +
|
||||
' exit 0\n' +
|
||||
')',
|
||||
};
|
||||
|
||||
export class UserScriptGenerator implements IUserScriptGenerator {
|
||||
public buildCode(selectedScripts: ReadonlyArray<SelectedScript>, version: string): IUserScript {
|
||||
if (!selectedScripts) { throw new Error('scripts is undefined'); }
|
||||
if (!version) { throw new Error('version is undefined'); }
|
||||
let scriptPositions = new Map<SelectedScript, ICodePosition>();
|
||||
if (!selectedScripts.length) {
|
||||
return { code: '', scriptPositions };
|
||||
}
|
||||
const builder = initializeCode(version);
|
||||
for (const selection of selectedScripts) {
|
||||
scriptPositions = appendSelection(selection, scriptPositions, builder);
|
||||
}
|
||||
const code = finalizeCode(builder);
|
||||
return { code, scriptPositions };
|
||||
}
|
||||
}
|
||||
|
||||
function initializeCode(version: string): CodeBuilder {
|
||||
return new CodeBuilder()
|
||||
.appendLine('@echo off')
|
||||
.appendCommentLine(`https://privacy.sexy — v${version} — ${new Date().toUTCString()}`)
|
||||
.appendFunction(adminRightsScript.name, adminRightsScript.code)
|
||||
.appendLine();
|
||||
}
|
||||
|
||||
function finalizeCode(builder: CodeBuilder): string {
|
||||
return builder.appendLine()
|
||||
.appendLine('pause')
|
||||
.appendLine('exit /b 0')
|
||||
.toString();
|
||||
}
|
||||
|
||||
function appendSelection(
|
||||
selection: SelectedScript,
|
||||
scriptPositions: Map<SelectedScript, ICodePosition>,
|
||||
builder: CodeBuilder): Map<SelectedScript, ICodePosition> {
|
||||
const startPosition = builder.currentLine + 1;
|
||||
appendCode(selection, builder);
|
||||
const endPosition = builder.currentLine - 1;
|
||||
builder.appendLine();
|
||||
scriptPositions.set(selection, new CodePosition(startPosition, endPosition));
|
||||
return scriptPositions;
|
||||
}
|
||||
|
||||
function appendCode(selection: SelectedScript, builder: CodeBuilder) {
|
||||
const name = selection.revert ? `${selection.script.name} (revert)` : selection.script.name;
|
||||
const scriptCode = selection.revert ? selection.script.revertCode : selection.script.code;
|
||||
builder.appendFunction(name, scriptCode);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ICodeChangedEvent } from './Event/ICodeChangedEvent';
|
||||
import { ISignal } from '@/infrastructure/Events/ISignal';
|
||||
|
||||
export interface IApplicationCode {
|
||||
readonly changed: ISignal<string>;
|
||||
readonly changed: ISignal<ICodeChangedEvent>;
|
||||
readonly current: string;
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
||||
|
||||
export interface IUserScriptGenerator {
|
||||
buildCode(selectedScripts: ReadonlyArray<SelectedScript>, version: string): string;
|
||||
}
|
||||
24
src/application/State/Code/Position/CodePosition.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { ICodePosition } from './ICodePosition';
|
||||
export class CodePosition implements ICodePosition {
|
||||
|
||||
public get totalLines(): number {
|
||||
return this.endLine - this.startLine;
|
||||
}
|
||||
|
||||
constructor(
|
||||
public readonly startLine: number,
|
||||
public readonly endLine: number) {
|
||||
if (startLine < 0) {
|
||||
throw new Error('Code cannot start in a negative line');
|
||||
}
|
||||
if (endLine < 0) {
|
||||
throw new Error('Code cannot end in a negative line');
|
||||
}
|
||||
if (endLine === startLine) {
|
||||
throw new Error('Empty code');
|
||||
}
|
||||
if (endLine < startLine) {
|
||||
throw new Error('End line cannot be less than start line');
|
||||
}
|
||||
}
|
||||
}
|
||||
5
src/application/State/Code/Position/ICodePosition.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface ICodePosition {
|
||||
readonly startLine: number;
|
||||
readonly endLine: number;
|
||||
readonly totalLines: number;
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
||||
import { IUserScriptGenerator } from './IUserScriptGenerator';
|
||||
import { CodeBuilder } from './CodeBuilder';
|
||||
|
||||
export const adminRightsScript = {
|
||||
name: 'Ensure admin privileges',
|
||||
code: 'fltmc >nul 2>&1 || (\n' +
|
||||
' echo This batch script requires administrator privileges. Right-click on\n' +
|
||||
' echo the script and select "Run as administrator".\n' +
|
||||
' pause\n' +
|
||||
' exit 1\n' +
|
||||
')',
|
||||
};
|
||||
|
||||
export class UserScriptGenerator implements IUserScriptGenerator {
|
||||
public buildCode(selectedScripts: ReadonlyArray<SelectedScript>, version: string): string {
|
||||
if (!selectedScripts) { throw new Error('scripts is undefined'); }
|
||||
if (!selectedScripts.length) { throw new Error('scripts are empty'); }
|
||||
if (!version) { throw new Error('version is undefined'); }
|
||||
const builder = new CodeBuilder()
|
||||
.appendLine('@echo off')
|
||||
.appendCommentLine(`https://privacy.sexy — v${version} — ${new Date().toUTCString()}`)
|
||||
.appendFunction(adminRightsScript.name, adminRightsScript.code).appendLine();
|
||||
for (const selection of selectedScripts) {
|
||||
const name = selection.revert ? `${selection.script.name} (revert)` : selection.script.name;
|
||||
const code = selection.revert ? selection.script.revertCode : selection.script.code;
|
||||
builder.appendFunction(name, code).appendLine();
|
||||
}
|
||||
return builder.appendLine()
|
||||
.appendLine('pause')
|
||||
.appendLine('exit /b 0')
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
@@ -6,11 +6,13 @@ export interface IUserSelection {
|
||||
readonly changed: ISignal<ReadonlyArray<SelectedScript>>;
|
||||
readonly selectedScripts: ReadonlyArray<SelectedScript>;
|
||||
readonly totalSelected: number;
|
||||
removeAllInCategory(categoryId: number): void;
|
||||
addOrUpdateAllInCategory(categoryId: number, revert: boolean): void;
|
||||
addSelectedScript(scriptId: string, revert: boolean): void;
|
||||
addOrUpdateSelectedScript(scriptId: string, revert: boolean): void;
|
||||
removeSelectedScript(scriptId: string): void;
|
||||
selectOnly(scripts: ReadonlyArray<IScript>): void;
|
||||
isSelected(script: IScript): boolean;
|
||||
isSelected(scriptId: string): boolean;
|
||||
selectAll(): void;
|
||||
deselectAll(): void;
|
||||
}
|
||||
|
||||
@@ -8,20 +8,49 @@ import { IRepository } from '@/infrastructure/Repository/IRepository';
|
||||
|
||||
export class UserSelection implements IUserSelection {
|
||||
public readonly changed = new Signal<ReadonlyArray<SelectedScript>>();
|
||||
private readonly scripts: IRepository<string, SelectedScript> = new InMemoryRepository<string, SelectedScript>();
|
||||
private readonly scripts: IRepository<string, SelectedScript>;
|
||||
|
||||
constructor(
|
||||
private readonly app: IApplication,
|
||||
/** Initially selected scripts */
|
||||
selectedScripts: ReadonlyArray<IScript>) {
|
||||
selectedScripts: ReadonlyArray<SelectedScript>) {
|
||||
this.scripts = new InMemoryRepository<string, SelectedScript>();
|
||||
if (selectedScripts && selectedScripts.length > 0) {
|
||||
for (const script of selectedScripts) {
|
||||
const selected = new SelectedScript(script, false);
|
||||
this.scripts.addItem(selected);
|
||||
this.scripts.addItem(script);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public removeAllInCategory(categoryId: number): void {
|
||||
const category = this.app.findCategory(categoryId);
|
||||
const scriptsToRemove = category.getAllScriptsRecursively()
|
||||
.filter((script) => this.scripts.exists(script.id));
|
||||
if (!scriptsToRemove.length) {
|
||||
return;
|
||||
}
|
||||
for (const script of scriptsToRemove) {
|
||||
this.scripts.removeItem(script.id);
|
||||
}
|
||||
this.changed.notify(this.scripts.getItems());
|
||||
}
|
||||
|
||||
public addOrUpdateAllInCategory(categoryId: number, revert: boolean = false): void {
|
||||
const category = this.app.findCategory(categoryId);
|
||||
const scriptsToAddOrUpdate = category.getAllScriptsRecursively()
|
||||
.filter((script) =>
|
||||
!this.scripts.exists(script.id)
|
||||
|| this.scripts.getById(script.id).revert !== revert,
|
||||
);
|
||||
if (!scriptsToAddOrUpdate.length) {
|
||||
return;
|
||||
}
|
||||
for (const script of scriptsToAddOrUpdate) {
|
||||
const selectedScript = new SelectedScript(script, revert);
|
||||
this.scripts.addOrUpdateItem(selectedScript);
|
||||
}
|
||||
this.changed.notify(this.scripts.getItems());
|
||||
}
|
||||
|
||||
public addSelectedScript(scriptId: string, revert: boolean): void {
|
||||
const script = this.app.findScript(scriptId);
|
||||
if (!script) {
|
||||
@@ -44,8 +73,8 @@ export class UserSelection implements IUserSelection {
|
||||
this.changed.notify(this.scripts.getItems());
|
||||
}
|
||||
|
||||
public isSelected(script: IScript): boolean {
|
||||
return this.scripts.exists(script.id);
|
||||
public isSelected(scriptId: string): boolean {
|
||||
return this.scripts.exists(scriptId);
|
||||
}
|
||||
|
||||
/** Get users scripts based on his/her selections */
|
||||
|
||||
@@ -35,12 +35,12 @@ function createWindow() {
|
||||
height: 955,
|
||||
webPreferences: {
|
||||
// Use pluginOptions.nodeIntegration, leave this alone
|
||||
// See nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-integration for more info
|
||||
// See https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-integration
|
||||
nodeIntegration: (process.env
|
||||
.ELECTRON_NODE_INTEGRATION as unknown) as boolean,
|
||||
},
|
||||
// https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/recipes.html#icons
|
||||
icon: path.join(__static, `favicon.ico`),
|
||||
// https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/recipes.html#set-tray-icon
|
||||
icon: path.join(__static, 'icon.png'),
|
||||
});
|
||||
|
||||
win.setMenuBarVisibility(false);
|
||||
|
||||
@@ -3,15 +3,7 @@ import { IScript } from './IScript';
|
||||
import { ICategory } from './ICategory';
|
||||
|
||||
export class Category extends BaseEntity<number> implements ICategory {
|
||||
private static validate(category: ICategory) {
|
||||
if (!category.name) {
|
||||
throw new Error('name is null or empty');
|
||||
}
|
||||
if ((!category.subCategories || category.subCategories.length === 0) &&
|
||||
(!category.scripts || category.scripts.length === 0)) {
|
||||
throw new Error('A category must have at least one sub-category or scripts');
|
||||
}
|
||||
}
|
||||
private allSubScripts: ReadonlyArray<IScript> = undefined;
|
||||
|
||||
constructor(
|
||||
id: number,
|
||||
@@ -20,6 +12,27 @@ export class Category extends BaseEntity<number> implements ICategory {
|
||||
public readonly subCategories?: ReadonlyArray<ICategory>,
|
||||
public readonly scripts?: ReadonlyArray<IScript>) {
|
||||
super(id);
|
||||
Category.validate(this);
|
||||
validateCategory(this);
|
||||
}
|
||||
|
||||
public getAllScriptsRecursively(): readonly IScript[] {
|
||||
return this.allSubScripts || (this.allSubScripts = parseScriptsRecursively(this));
|
||||
}
|
||||
}
|
||||
|
||||
function parseScriptsRecursively(category: ICategory): ReadonlyArray<IScript> {
|
||||
return [
|
||||
...category.scripts,
|
||||
...category.subCategories.flatMap((c) => c.getAllScriptsRecursively()),
|
||||
];
|
||||
}
|
||||
|
||||
function validateCategory(category: ICategory) {
|
||||
if (!category.name) {
|
||||
throw new Error('undefined or empty name');
|
||||
}
|
||||
if ((!category.subCategories || category.subCategories.length === 0) &&
|
||||
(!category.scripts || category.scripts.length === 0)) {
|
||||
throw new Error('A category must have at least one sub-category or script');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ export interface ICategory extends IEntity<number>, IDocumentable {
|
||||
readonly name: string;
|
||||
readonly subCategories?: ReadonlyArray<ICategory>;
|
||||
readonly scripts?: ReadonlyArray<IScript>;
|
||||
getAllScriptsRecursively(): ReadonlyArray<IScript>;
|
||||
}
|
||||
|
||||
export { IEntity } from '../infrastructure/Entity/IEntity';
|
||||
|
||||
@@ -3,6 +3,7 @@ import { IEntity } from '../Entity/IEntity';
|
||||
export interface IRepository<TKey, TEntity extends IEntity<TKey>> {
|
||||
readonly length: number;
|
||||
getItems(predicate?: (entity: TEntity) => boolean): TEntity[];
|
||||
getById(id: TKey): TEntity | undefined;
|
||||
addItem(item: TEntity): void;
|
||||
addOrUpdateItem(item: TEntity): void;
|
||||
removeItem(id: TKey): void;
|
||||
|
||||
@@ -16,6 +16,14 @@ export class InMemoryRepository<TKey, TEntity extends IEntity<TKey>> implements
|
||||
return predicate ? this.items.filter(predicate) : this.items;
|
||||
}
|
||||
|
||||
public getById(id: TKey): TEntity | undefined {
|
||||
const items = this.getItems((entity) => entity.id === id);
|
||||
if (!items.length) {
|
||||
return undefined;
|
||||
}
|
||||
return items[0];
|
||||
}
|
||||
|
||||
public addItem(item: TEntity): void {
|
||||
if (!item) {
|
||||
throw new Error('item is null or undefined');
|
||||
|
||||
@@ -8,10 +8,8 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue, Emit } from 'vue-property-decorator';
|
||||
import { Component, Prop, Emit } from 'vue-property-decorator';
|
||||
import { StatefulVue } from './StatefulVue';
|
||||
import { SaveFileDialog } from './../infrastructure/SaveFileDialog';
|
||||
import { Clipboard } from './../infrastructure/Clipboard';
|
||||
|
||||
@Component
|
||||
export default class IconButton extends StatefulVue {
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import { Component } from 'vue-property-decorator';
|
||||
import CardListItem from './CardListItem.vue';
|
||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||
import { ICategory } from '@/domain/ICategory';
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue, Watch, Emit } from 'vue-property-decorator';
|
||||
import { Component, Prop, Watch, Emit } from 'vue-property-decorator';
|
||||
import ScriptsTree from '@/presentation/Scripts/ScriptsTree/ScriptsTree.vue';
|
||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||
|
||||
|
||||
@@ -15,9 +15,8 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import { Component } from 'vue-property-decorator';
|
||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||
import { IApplicationState } from '@/application/State/IApplicationState';
|
||||
import { Grouping } from './Grouping';
|
||||
|
||||
const DefaultGrouping = Grouping.Cards;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { IApplication } from './../../../domain/IApplication';
|
||||
import { ICategory, IScript } from '@/domain/ICategory';
|
||||
import { INode } from './SelectableTree/INode';
|
||||
import { INode, NodeType } from './SelectableTree/Node/INode';
|
||||
|
||||
export function parseAllCategories(app: IApplication): INode[] | undefined {
|
||||
const nodes = new Array<INode>();
|
||||
@@ -23,9 +23,15 @@ export function parseSingleCategory(categoryId: number, app: IApplication): INod
|
||||
export function getScriptNodeId(script: IScript): string {
|
||||
return script.id;
|
||||
}
|
||||
export function getScriptId(nodeId: string): string {
|
||||
return nodeId;
|
||||
}
|
||||
export function getCategoryId(nodeId: string): number {
|
||||
return +nodeId;
|
||||
}
|
||||
|
||||
export function getCategoryNodeId(category: ICategory): string {
|
||||
return `Category${category.id}`;
|
||||
return `${category.id}`;
|
||||
}
|
||||
|
||||
function parseCategoryRecursively(
|
||||
@@ -64,16 +70,18 @@ function convertCategoryToNode(
|
||||
category: ICategory, children: readonly INode[]): INode {
|
||||
return {
|
||||
id: getCategoryNodeId(category),
|
||||
type: NodeType.Category,
|
||||
text: category.name,
|
||||
children,
|
||||
documentationUrls: category.documentationUrls,
|
||||
isReversible: false,
|
||||
isReversible: children && children.every((child) => child.isReversible),
|
||||
};
|
||||
}
|
||||
|
||||
function convertScriptToNode(script: IScript): INode {
|
||||
return {
|
||||
id: getScriptNodeId(script),
|
||||
type: NodeType.Script,
|
||||
text: script.name,
|
||||
children: undefined,
|
||||
documentationUrls: script.documentationUrls,
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
:filterPredicate="filterPredicate"
|
||||
:filterText="filterText"
|
||||
v-on:nodeSelected="toggleNodeSelectionAsync($event)"
|
||||
v-on:nodeRevertToggled="handleNodeRevertToggleAsync($event)"
|
||||
>
|
||||
</SelectableTree>
|
||||
</span>
|
||||
@@ -16,18 +15,17 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
|
||||
import { Component, Prop, Watch } from 'vue-property-decorator';
|
||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||
import { Category } from '@/domain/Category';
|
||||
import { IRepository } from '@/infrastructure/Repository/IRepository';
|
||||
import { IScript } from '@/domain/IScript';
|
||||
import { ICategory } from '@/domain/ICategory';
|
||||
import { IApplicationState, IUserSelection } from '@/application/State/IApplicationState';
|
||||
import { IApplicationState } from '@/application/State/IApplicationState';
|
||||
import { IFilterResult } from '@/application/State/Filter/IFilterResult';
|
||||
import { parseAllCategories, parseSingleCategory, getScriptNodeId, getCategoryNodeId } from './ScriptNodeParser';
|
||||
import SelectableTree, { FilterPredicate } from './SelectableTree/SelectableTree.vue';
|
||||
import { INode } from './SelectableTree/INode';
|
||||
import { parseAllCategories, parseSingleCategory, getScriptNodeId, getCategoryNodeId, getCategoryId, getScriptId } from './ScriptNodeParser';
|
||||
import SelectableTree from './SelectableTree/SelectableTree.vue';
|
||||
import { INode, NodeType } from './SelectableTree/Node/INode';
|
||||
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
||||
import { INodeSelectedEvent } from './SelectableTree/INodeSelectedEvent';
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
@@ -53,15 +51,17 @@
|
||||
await this.initializeNodesAsync(this.categoryId);
|
||||
}
|
||||
|
||||
public async toggleNodeSelectionAsync(node: INode) {
|
||||
if (node.children != null && node.children.length > 0) {
|
||||
return; // only interested in script nodes
|
||||
}
|
||||
public async toggleNodeSelectionAsync(event: INodeSelectedEvent) {
|
||||
const state = await this.getCurrentStateAsync();
|
||||
if (!this.selectedNodeIds.some((id) => id === node.id)) {
|
||||
state.selection.addSelectedScript(node.id, false);
|
||||
} else {
|
||||
state.selection.removeSelectedScript(node.id);
|
||||
switch (event.node.type) {
|
||||
case NodeType.Category:
|
||||
toggleCategoryNodeSelection(event, state);
|
||||
break;
|
||||
case NodeType.Script:
|
||||
toggleScriptNodeSelection(event, state);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown node type: ${event.node.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,6 +99,24 @@
|
||||
}
|
||||
}
|
||||
|
||||
function toggleCategoryNodeSelection(event: INodeSelectedEvent, state: IApplicationState): void {
|
||||
const categoryId = getCategoryId(event.node.id);
|
||||
if (event.isSelected) {
|
||||
state.selection.addOrUpdateAllInCategory(categoryId, false);
|
||||
} else {
|
||||
state.selection.removeAllInCategory(categoryId);
|
||||
}
|
||||
}
|
||||
function toggleScriptNodeSelection(event: INodeSelectedEvent, state: IApplicationState): void {
|
||||
const scriptId = getScriptId(event.node.id);
|
||||
const actualToggleState = state.selection.isSelected(scriptId);
|
||||
const targetToggleState = event.isSelected;
|
||||
if (targetToggleState && !actualToggleState) {
|
||||
state.selection.addSelectedScript(scriptId, false);
|
||||
} else if (!targetToggleState && actualToggleState) {
|
||||
state.selection.removeSelectedScript(scriptId);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { INode } from './Node/INode';
|
||||
|
||||
export interface INodeSelectedEvent {
|
||||
isSelected: boolean;
|
||||
node: INode;
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
// Two ways of typing other libraries: https://stackoverflow.com/a/53070501
|
||||
|
||||
declare module 'liquor-tree' {
|
||||
import { PluginObject } from 'vue';
|
||||
@@ -10,33 +9,43 @@ declare module 'liquor-tree' {
|
||||
filter(query: string): void;
|
||||
clearFilter(): void;
|
||||
setModel(nodes: ReadonlyArray<ILiquorTreeNewNode>): void;
|
||||
// getNodeById(id: string): ILiquorTreeExistingNode;
|
||||
recurseDown(fn: (node: ILiquorTreeExistingNode) => void): void;
|
||||
}
|
||||
interface ICustomLiquorTreeData {
|
||||
export interface ICustomLiquorTreeData {
|
||||
type: number;
|
||||
documentationUrls: ReadonlyArray<string>;
|
||||
isReversible: boolean;
|
||||
}
|
||||
// https://github.com/amsik/liquor-tree/blob/master/src/lib/Node.js
|
||||
export interface ILiquorTreeNodeState {
|
||||
checked: boolean;
|
||||
}
|
||||
|
||||
export interface ILiquorTreeNode {
|
||||
id: string;
|
||||
data: ICustomLiquorTreeData;
|
||||
children: ReadonlyArray<ILiquorTreeNode> | undefined;
|
||||
}
|
||||
/**
|
||||
* Returned from Node tree view events.
|
||||
* See constructor in https://github.com/amsik/liquor-tree/blob/master/src/lib/Node.js
|
||||
*/
|
||||
export interface ILiquorTreeExistingNode {
|
||||
id: string;
|
||||
export interface ILiquorTreeExistingNode extends ILiquorTreeNode {
|
||||
data: ILiquorTreeNodeData;
|
||||
states: ILiquorTreeNodeState | undefined;
|
||||
children: ReadonlyArray<ILiquorTreeExistingNode> | undefined;
|
||||
// expand(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sent to liquor tree to define of new nodes.
|
||||
* https://github.com/amsik/liquor-tree/blob/master/src/lib/Node.js
|
||||
*/
|
||||
export interface ILiquorTreeNewNode {
|
||||
id: string;
|
||||
export interface ILiquorTreeNewNode extends ILiquorTreeNode {
|
||||
text: string;
|
||||
state: ILiquorTreeNodeState | undefined;
|
||||
children: ReadonlyArray<ILiquorTreeNewNode> | undefined;
|
||||
data: ICustomLiquorTreeData;
|
||||
}
|
||||
|
||||
// https://amsik.github.io/liquor-tree/#Component-Options
|
||||
@@ -47,29 +56,16 @@ declare module 'liquor-tree' {
|
||||
autoCheckChildren: boolean;
|
||||
parentSelect: boolean;
|
||||
keyboardNavigation: boolean;
|
||||
deletion: (node: ILiquorTreeExistingNode) => void;
|
||||
filter: ILiquorTreeFilter;
|
||||
deletion(node: ILiquorTreeNode): boolean;
|
||||
}
|
||||
|
||||
// https://github.com/amsik/liquor-tree/blob/master/src/lib/Node.js
|
||||
interface ILiquorTreeNodeState {
|
||||
checked: boolean;
|
||||
}
|
||||
|
||||
interface ILiquorTreeNodeData extends ICustomLiquorTreeData {
|
||||
export interface ILiquorTreeNodeData extends ICustomLiquorTreeData {
|
||||
text: string;
|
||||
}
|
||||
|
||||
// https://github.com/amsik/liquor-tree/blob/master/src/components/TreeRoot.vue
|
||||
interface ILiquorTreeOptions {
|
||||
checkbox: boolean;
|
||||
checkOnSelect: boolean;
|
||||
filter: ILiquorTreeFilter;
|
||||
deletion(node: ILiquorTreeNewNode): boolean;
|
||||
}
|
||||
|
||||
// https://github.com/amsik/liquor-tree/blob/master/src/components/TreeRoot.vue
|
||||
interface ILiquorTreeFilter {
|
||||
export interface ILiquorTreeFilter {
|
||||
emptyText: string;
|
||||
matcher(query: string, node: ILiquorTreeExistingNode): boolean;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { ILiquorTreeOptions, ILiquorTreeFilter, ILiquorTreeNode, ILiquorTreeExistingNode } from 'liquor-tree';
|
||||
|
||||
export class LiquorTreeOptions implements ILiquorTreeOptions {
|
||||
public readonly multiple = true;
|
||||
public readonly checkbox = true;
|
||||
public readonly checkOnSelect = true;
|
||||
/* For checkbox mode only. Children will have the same checked state as their parent.
|
||||
This is false as it's handled manually to be able to batch select for performance + highlighting */
|
||||
public readonly autoCheckChildren = false;
|
||||
public readonly parentSelect = false;
|
||||
public readonly keyboardNavigation = true;
|
||||
public readonly filter = { // Wrap this in an arrow function as setting filter directly does not work JS APIs
|
||||
emptyText: this.liquorTreeFilter.emptyText,
|
||||
matcher: (query: string, node: ILiquorTreeExistingNode) => {
|
||||
return this.liquorTreeFilter.matcher(query, node);
|
||||
},
|
||||
};
|
||||
constructor(private readonly liquorTreeFilter: ILiquorTreeFilter) { }
|
||||
public deletion(node: ILiquorTreeNode): boolean {
|
||||
return false; // no op
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { ILiquorTreeFilter, ILiquorTreeExistingNode } from 'liquor-tree';
|
||||
import { convertExistingToNode } from './NodeTranslator';
|
||||
import { INode } from './../../Node/INode';
|
||||
|
||||
export type FilterPredicate = (node: INode) => boolean;
|
||||
|
||||
export class NodePredicateFilter implements ILiquorTreeFilter {
|
||||
public emptyText = ''; // Does not matter as a custom mesage is shown
|
||||
constructor(private readonly filterPredicate: FilterPredicate) {
|
||||
if (!filterPredicate) {
|
||||
throw new Error('filterPredicate is undefined');
|
||||
}
|
||||
}
|
||||
public matcher(query: string, node: ILiquorTreeExistingNode): boolean {
|
||||
return this.filterPredicate(convertExistingToNode(node));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { ILiquorTreeNode } from 'liquor-tree';
|
||||
import { NodeType } from './../../Node/INode';
|
||||
|
||||
export function getNewCheckedState(
|
||||
oldNode: ILiquorTreeNode,
|
||||
selectedNodeIds: ReadonlyArray<string>): boolean {
|
||||
switch (oldNode.data.type) {
|
||||
case NodeType.Script:
|
||||
return selectedNodeIds.some((id) => id === oldNode.id);
|
||||
case NodeType.Category:
|
||||
return parseAllSubScriptIds(oldNode).every((id) => selectedNodeIds.includes(id));
|
||||
default:
|
||||
throw new Error('Unknown node type');
|
||||
}
|
||||
}
|
||||
|
||||
function parseAllSubScriptIds(categoryNode: ILiquorTreeNode): ReadonlyArray<string> {
|
||||
if (categoryNode.data.type !== NodeType.Category) {
|
||||
throw new Error('Not a category node');
|
||||
}
|
||||
if (!categoryNode.children) {
|
||||
return [];
|
||||
}
|
||||
return categoryNode
|
||||
.children
|
||||
.flatMap((child) => getNodeIds(child));
|
||||
}
|
||||
|
||||
function getNodeIds(node: ILiquorTreeNode): ReadonlyArray<string> {
|
||||
switch (node.data.type) {
|
||||
case NodeType.Script:
|
||||
return [ node.id ];
|
||||
case NodeType.Category:
|
||||
return parseAllSubScriptIds(node);
|
||||
default:
|
||||
throw new Error('Unknown node type');
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ILiquorTreeNewNode, ILiquorTreeExistingNode } from 'liquor-tree';
|
||||
import { INode } from './INode';
|
||||
import { INode } from './../../Node/INode';
|
||||
|
||||
// Functions to translate INode to LiqourTree models and vice versa for anti-corruption
|
||||
|
||||
@@ -7,6 +7,7 @@ export function convertExistingToNode(liquorTreeNode: ILiquorTreeExistingNode):
|
||||
if (!liquorTreeNode) { throw new Error('liquorTreeNode is undefined'); }
|
||||
return {
|
||||
id: liquorTreeNode.id,
|
||||
type: liquorTreeNode.data.type,
|
||||
text: liquorTreeNode.data.text,
|
||||
// selected: liquorTreeNode.states && liquorTreeNode.states.checked,
|
||||
children: convertChildren(liquorTreeNode.children, convertExistingToNode),
|
||||
@@ -27,6 +28,7 @@ export function toNewLiquorTreeNode(node: INode): ILiquorTreeNewNode {
|
||||
data: {
|
||||
documentationUrls: node.documentationUrls,
|
||||
isReversible: node.isReversible,
|
||||
type: node.type,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,7 +1,13 @@
|
||||
export enum NodeType {
|
||||
Script,
|
||||
Category,
|
||||
}
|
||||
|
||||
export interface INode {
|
||||
readonly id: string;
|
||||
readonly text: string;
|
||||
readonly isReversible: boolean;
|
||||
readonly documentationUrls: ReadonlyArray<string>;
|
||||
readonly children?: ReadonlyArray<INode>;
|
||||
readonly type: NodeType;
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
<RevertToggle
|
||||
class="item"
|
||||
v-if="data.isReversible"
|
||||
:scriptId="data.id" />
|
||||
:node="data" />
|
||||
<DocumentationUrls
|
||||
class="item"
|
||||
v-if="data.documentationUrls && data.documentationUrls.length > 0"
|
||||
@@ -0,0 +1,153 @@
|
||||
<template>
|
||||
<div class="checkbox-switch" >
|
||||
<input type="checkbox" class="input-checkbox"
|
||||
v-model="isReverted"
|
||||
@change="onRevertToggledAsync()"
|
||||
v-on:click.stop>
|
||||
<div class="checkbox-animate">
|
||||
<span class="checkbox-off">revert</span>
|
||||
<span class="checkbox-on">revert</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Watch } from 'vue-property-decorator';
|
||||
import { IReverter } from './Reverter/IReverter';
|
||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||
import { INode } from './INode';
|
||||
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
||||
import { IApplicationState } from '@/application/State/IApplicationState';
|
||||
import { getCategoryId, getScriptId } from './../../ScriptNodeParser';
|
||||
import { getReverter } from './Reverter/ReverterFactory';
|
||||
|
||||
@Component
|
||||
export default class RevertToggle extends StatefulVue {
|
||||
@Prop() public node: INode;
|
||||
public isReverted = false;
|
||||
|
||||
private handler: IReverter;
|
||||
|
||||
public async mounted() {
|
||||
await this.onNodeChangedAsync(this.node);
|
||||
const state = await this.getCurrentStateAsync();
|
||||
this.updateState(state.selection.selectedScripts);
|
||||
state.selection.changed.on((scripts) => this.updateState(scripts));
|
||||
}
|
||||
|
||||
@Watch('node') public async onNodeChangedAsync(node: INode) {
|
||||
const state = await this.getCurrentStateAsync();
|
||||
this.handler = getReverter(node, state.app);
|
||||
}
|
||||
|
||||
public async onRevertToggledAsync() {
|
||||
const state = await this.getCurrentStateAsync();
|
||||
this.handler.selectWithRevertState(this.isReverted, state.selection);
|
||||
}
|
||||
|
||||
private updateState(scripts: ReadonlyArray<SelectedScript>) {
|
||||
this.isReverted = this.handler.getState(scripts);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/presentation/styles/colors.scss";
|
||||
$width: 85px;
|
||||
$height: 30px;
|
||||
// https://www.designlabthemes.com/css-toggle-switch/
|
||||
.checkbox-switch {
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: $width;
|
||||
height: $height;
|
||||
-webkit-border-radius: $height;
|
||||
border-radius: $height;
|
||||
line-height: $height;
|
||||
font-size: $height / 2;
|
||||
display: inline-block;
|
||||
|
||||
input.input-checkbox {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: $width;
|
||||
height: $height;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
opacity: 0;
|
||||
z-index: 2;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checkbox-animate {
|
||||
position: relative;
|
||||
width: $width;
|
||||
height: $height;
|
||||
background-color: $gray;
|
||||
-webkit-transition: background-color 0.25s ease-out 0s;
|
||||
transition: background-color 0.25s ease-out 0s;
|
||||
|
||||
// Circle
|
||||
&:before {
|
||||
$circle-size: $height * 0.66;
|
||||
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: $circle-size;
|
||||
height: $circle-size;
|
||||
border-radius: $circle-size * 2;
|
||||
-webkit-border-radius: $circle-size * 2;
|
||||
background-color: $slate;
|
||||
top: $height * 0.16;
|
||||
left: $width * 0.05;
|
||||
-webkit-transition: left 0.3s ease-out 0s;
|
||||
transition: left 0.3s ease-out 0s;
|
||||
z-index: 10;
|
||||
}
|
||||
}
|
||||
|
||||
input.input-checkbox:checked {
|
||||
+ .checkbox-animate {
|
||||
background-color: $accent;
|
||||
}
|
||||
+ .checkbox-animate:before {
|
||||
left: ($width - $width/3.5);
|
||||
background-color: $light-gray;
|
||||
}
|
||||
+ .checkbox-animate .checkbox-off {
|
||||
display: none;
|
||||
opacity: 0;
|
||||
}
|
||||
+ .checkbox-animate .checkbox-on {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox-off, .checkbox-on {
|
||||
float: left;
|
||||
color: $white;
|
||||
font-weight: 700;
|
||||
-webkit-transition: all 0.3s ease-out 0s;
|
||||
transition: all 0.3s ease-out 0s;
|
||||
}
|
||||
|
||||
.checkbox-off {
|
||||
margin-left: $width / 3;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.checkbox-on {
|
||||
display: none;
|
||||
float: right;
|
||||
margin-right: $width / 3;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,30 @@
|
||||
import { IReverter } from './IReverter';
|
||||
import { getCategoryId } from '../../../ScriptNodeParser';
|
||||
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
import { ScriptReverter } from './ScriptReverter';
|
||||
import { IUserSelection } from '@/application/State/Selection/IUserSelection';
|
||||
|
||||
export class CategoryReverter implements IReverter {
|
||||
private readonly categoryId: number;
|
||||
private readonly scriptReverters: ReadonlyArray<ScriptReverter>;
|
||||
constructor(nodeId: string, app: IApplication) {
|
||||
this.categoryId = getCategoryId(nodeId);
|
||||
this.scriptReverters = getAllSubScriptReverters(this.categoryId, app);
|
||||
}
|
||||
public getState(selectedScripts: ReadonlyArray<SelectedScript>): boolean {
|
||||
return this.scriptReverters.every((script) => script.getState(selectedScripts));
|
||||
}
|
||||
public selectWithRevertState(newState: boolean, selection: IUserSelection): void {
|
||||
selection.addOrUpdateAllInCategory(this.categoryId, newState);
|
||||
}
|
||||
}
|
||||
|
||||
function getAllSubScriptReverters(categoryId: number, app: IApplication) {
|
||||
const category = app.findCategory(categoryId);
|
||||
if (!category) {
|
||||
throw new Error(`Category with id "${categoryId}" does not exist`);
|
||||
}
|
||||
const scripts = category.getAllScriptsRecursively();
|
||||
return scripts.map((script) => new ScriptReverter(script.id));
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
||||
import { IUserSelection } from '@/application/State/IApplicationState';
|
||||
|
||||
export interface IReverter {
|
||||
getState(selectedScripts: ReadonlyArray<SelectedScript>): boolean;
|
||||
selectWithRevertState(newState: boolean, selection: IUserSelection): void;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { INode, NodeType } from '../INode';
|
||||
import { IReverter } from './IReverter';
|
||||
import { ScriptReverter } from './ScriptReverter';
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
import { CategoryReverter } from './CategoryReverter';
|
||||
|
||||
export function getReverter(node: INode, app: IApplication): IReverter {
|
||||
switch (node.type) {
|
||||
case NodeType.Category:
|
||||
return new CategoryReverter(node.id, app);
|
||||
case NodeType.Script:
|
||||
return new ScriptReverter(node.id);
|
||||
default:
|
||||
throw new Error('Unknown script type');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { IReverter } from './IReverter';
|
||||
import { getScriptId } from '../../../ScriptNodeParser';
|
||||
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
||||
import { IUserSelection } from '@/application/State/IApplicationState';
|
||||
|
||||
export class ScriptReverter implements IReverter {
|
||||
private readonly scriptId: string;
|
||||
constructor(nodeId: string) {
|
||||
this.scriptId = getScriptId(nodeId);
|
||||
}
|
||||
public getState(selectedScripts: ReadonlyArray<SelectedScript>): boolean {
|
||||
const selectedScript = selectedScripts.find((selected) => selected.id === this.scriptId);
|
||||
if (!selectedScript) {
|
||||
return false;
|
||||
}
|
||||
return selectedScript.revert;
|
||||
}
|
||||
public selectWithRevertState(newState: boolean, selection: IUserSelection): void {
|
||||
selection.addOrUpdateSelectedScript(this.scriptId, newState);
|
||||
}
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
<template>
|
||||
<div class="checkbox-switch" >
|
||||
<input type="checkbox" class="input-checkbox"
|
||||
v-model="isReverted"
|
||||
@change="onRevertToggledAsync()" >
|
||||
<div class="checkbox-animate">
|
||||
<span class="checkbox-off">revert</span>
|
||||
<span class="checkbox-on">revert</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||
import { INode } from './INode';
|
||||
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
||||
|
||||
@Component
|
||||
export default class RevertToggle extends StatefulVue {
|
||||
@Prop() public scriptId: string;
|
||||
public isReverted = false;
|
||||
public async mounted() {
|
||||
const state = await this.getCurrentStateAsync();
|
||||
state.selection.changed.on(this.handleSelectionChanged);
|
||||
}
|
||||
public async onRevertToggledAsync() {
|
||||
const state = await this.getCurrentStateAsync();
|
||||
state.selection.addOrUpdateSelectedScript(this.scriptId, this.isReverted);
|
||||
}
|
||||
private handleSelectionChanged(selectedScripts: ReadonlyArray<SelectedScript>): void {
|
||||
const selectedScript = selectedScripts.find((script) => script.id === this.scriptId);
|
||||
if (!selectedScript) {
|
||||
this.isReverted = false;
|
||||
} else {
|
||||
this.isReverted = selectedScript.revert;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/presentation/styles/colors.scss";
|
||||
$width: 85px;
|
||||
$height: 30px;
|
||||
// https://www.designlabthemes.com/css-toggle-switch/
|
||||
.checkbox-switch {
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: $width;
|
||||
height: $height;
|
||||
-webkit-border-radius: $height;
|
||||
border-radius: $height;
|
||||
line-height: $height;
|
||||
font-size: $height / 2;
|
||||
display: inline-block;
|
||||
|
||||
input.input-checkbox {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: $width;
|
||||
height: $height;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
opacity: 0;
|
||||
z-index: 2;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checkbox-animate {
|
||||
position: relative;
|
||||
width: $width;
|
||||
height: $height;
|
||||
background-color: $gray;
|
||||
-webkit-transition: background-color 0.25s ease-out 0s;
|
||||
transition: background-color 0.25s ease-out 0s;
|
||||
|
||||
// Circle
|
||||
&:before {
|
||||
$circle-size: $height * 0.66;
|
||||
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: $circle-size;
|
||||
height: $circle-size;
|
||||
border-radius: $circle-size * 2;
|
||||
-webkit-border-radius: $circle-size * 2;
|
||||
background-color: $slate;
|
||||
top: $height * 0.16;
|
||||
left: $width * 0.05;
|
||||
-webkit-transition: left 0.3s ease-out 0s;
|
||||
transition: left 0.3s ease-out 0s;
|
||||
z-index: 10;
|
||||
}
|
||||
}
|
||||
|
||||
input.input-checkbox:checked {
|
||||
+ .checkbox-animate {
|
||||
background-color: $accent;
|
||||
}
|
||||
+ .checkbox-animate:before {
|
||||
left: ($width - $width/3.5);
|
||||
background-color: $light-gray;
|
||||
}
|
||||
+ .checkbox-animate .checkbox-off {
|
||||
display: none;
|
||||
opacity: 0;
|
||||
}
|
||||
+ .checkbox-animate .checkbox-on {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox-off, .checkbox-on {
|
||||
float: left;
|
||||
color: $white;
|
||||
font-weight: 700;
|
||||
-webkit-transition: all 0.3s ease-out 0s;
|
||||
transition: all 0.3s ease-out 0s;
|
||||
}
|
||||
|
||||
.checkbox-off {
|
||||
margin-left: $width / 3;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.checkbox-on {
|
||||
display: none;
|
||||
float: right;
|
||||
margin-right: $width / 3;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -7,7 +7,7 @@
|
||||
v-on:node:unchecked="nodeSelected($event)"
|
||||
ref="treeElement"
|
||||
>
|
||||
<span class="tree-text" slot-scope="{ node }">
|
||||
<span class="tree-text" slot-scope="{ node }" >
|
||||
<Node :data="convertExistingToNode(node)" />
|
||||
</span>
|
||||
</tree>
|
||||
@@ -17,18 +17,21 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue, Emit, Watch } from 'vue-property-decorator';
|
||||
import LiquorTree, { ILiquorTreeNewNode, ILiquorTreeExistingNode, ILiquorTree, ILiquorTreeOptions } from 'liquor-tree';
|
||||
import Node from './Node.vue';
|
||||
import { INode } from './INode';
|
||||
import { convertExistingToNode, toNewLiquorTreeNode } from './NodeTranslator';
|
||||
export type FilterPredicate = (node: INode) => boolean;
|
||||
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
|
||||
import LiquorTree, { ILiquorTreeNewNode, ILiquorTreeExistingNode, ILiquorTree } from 'liquor-tree';
|
||||
import Node from './Node/Node.vue';
|
||||
import { INode } from './Node/INode';
|
||||
import { convertExistingToNode, toNewLiquorTreeNode } from './LiquorTree/NodeWrapper/NodeTranslator';
|
||||
import { INodeSelectedEvent } from './/INodeSelectedEvent';
|
||||
import { getNewCheckedState } from './LiquorTree/NodeWrapper/NodeStateUpdater';
|
||||
import { LiquorTreeOptions } from './LiquorTree/LiquorTreeOptions';
|
||||
import { FilterPredicate, NodePredicateFilter } from './LiquorTree/NodeWrapper/NodePredicateFilter';
|
||||
|
||||
/** Wrapper for Liquor Tree, reveals only abstracted INode for communication */
|
||||
@Component({
|
||||
components: {
|
||||
LiquorTree,
|
||||
Node,
|
||||
LiquorTree,
|
||||
Node,
|
||||
},
|
||||
})
|
||||
export default class SelectableTree extends Vue {
|
||||
@@ -38,7 +41,7 @@
|
||||
@Prop() public initialNodes?: ReadonlyArray<INode>;
|
||||
|
||||
public initialLiquourTreeNodes?: ILiquorTreeNewNode[] = null;
|
||||
public liquorTreeOptions = this.getDefaults();
|
||||
public liquorTreeOptions = new LiquorTreeOptions(new NodePredicateFilter((node) => this.filterPredicate(node)));
|
||||
public convertExistingToNode = convertExistingToNode;
|
||||
|
||||
public mounted() {
|
||||
@@ -46,7 +49,7 @@
|
||||
const initialNodes = this.initialNodes.map((node) => toNewLiquorTreeNode(node));
|
||||
if (this.selectedNodeIds) {
|
||||
recurseDown(initialNodes,
|
||||
(node) => node.state.checked = this.selectedNodeIds.includes(node.id));
|
||||
(node) => node.state.checked = getNewCheckedState(node, this.selectedNodeIds));
|
||||
}
|
||||
this.initialLiquourTreeNodes = initialNodes;
|
||||
} else {
|
||||
@@ -58,7 +61,11 @@
|
||||
}
|
||||
|
||||
public nodeSelected(node: ILiquorTreeExistingNode) {
|
||||
this.$emit('nodeSelected', convertExistingToNode(node));
|
||||
const event: INodeSelectedEvent = {
|
||||
node: convertExistingToNode(node),
|
||||
isSelected: node.states.checked,
|
||||
};
|
||||
this.$emit('nodeSelected', event);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -73,23 +80,13 @@
|
||||
}
|
||||
|
||||
@Watch('selectedNodeIds')
|
||||
public setSelectedStatus(selectedNodeIds: ReadonlyArray<string>) {
|
||||
public setSelectedStatusAsync(selectedNodeIds: ReadonlyArray<string>) {
|
||||
if (!selectedNodeIds) {
|
||||
throw new Error('Selected nodes are undefined');
|
||||
}
|
||||
const newNodes = updateCheckedState(this.getLiquorTreeApi().model, selectedNodeIds);
|
||||
this.getLiquorTreeApi().setModel(newNodes);
|
||||
/* Alternative:
|
||||
this.getLiquorTreeApi().recurseDown((node) => {
|
||||
node.states.checked = selectedNodeIds.includes(node.id);
|
||||
});
|
||||
Problem: Does not check their parent if all children are checked, because it does not
|
||||
trigger update on parent as we work with scripts not categories. */
|
||||
/* Alternative:
|
||||
this.getLiquorTreeApi().recurseDown((node) => {
|
||||
if(selectedNodeIds.includes(node.id)) { node.select(); } else { node.unselect(); }
|
||||
});
|
||||
Problem: Emits nodeSelected() event again which will cause an infinite loop. */
|
||||
this.getLiquorTreeApi().recurseDown((node) => {
|
||||
node.states.checked = getNewCheckedState(node, selectedNodeIds);
|
||||
});
|
||||
}
|
||||
|
||||
private getLiquorTreeApi(): ILiquorTree {
|
||||
@@ -98,27 +95,6 @@
|
||||
}
|
||||
return (this.$refs.treeElement as any).tree;
|
||||
}
|
||||
|
||||
private getDefaults(): ILiquorTreeOptions {
|
||||
return {
|
||||
multiple: true,
|
||||
checkbox: true,
|
||||
checkOnSelect: true,
|
||||
autoCheckChildren: true,
|
||||
parentSelect: false,
|
||||
keyboardNavigation: true,
|
||||
deletion: (node) => !node.children || node.children.length === 0,
|
||||
filter: {
|
||||
matcher: (query: string, node: ILiquorTreeExistingNode) => {
|
||||
if (!this.filterPredicate) {
|
||||
throw new Error('Cannot filter as predicate is null');
|
||||
}
|
||||
return this.filterPredicate(convertExistingToNode(node));
|
||||
},
|
||||
emptyText: '🕵️Hmm.. Can not see one 🧐',
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function recurseDown(
|
||||
@@ -131,27 +107,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateCheckedState(
|
||||
oldNodes: ReadonlyArray<ILiquorTreeExistingNode>,
|
||||
selectedNodeIds: ReadonlyArray<string>): ReadonlyArray<ILiquorTreeNewNode> {
|
||||
const result = new Array<ILiquorTreeNewNode>();
|
||||
for (const oldNode of oldNodes) {
|
||||
const newState = oldNode.states;
|
||||
newState.checked = selectedNodeIds.some((id) => id === oldNode.id);
|
||||
const newNode: ILiquorTreeNewNode = {
|
||||
id: oldNode.id,
|
||||
text: oldNode.data.text,
|
||||
data: {
|
||||
documentationUrls: oldNode.data.documentationUrls,
|
||||
isReversible: oldNode.data.isReversible,
|
||||
},
|
||||
children: oldNode.children == null ? [] :
|
||||
updateCheckedState(oldNode.children, selectedNodeIds),
|
||||
state: newState,
|
||||
};
|
||||
result.push(newNode);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue, Emit } from 'vue-property-decorator';
|
||||
import { Component, Prop, Emit } from 'vue-property-decorator';
|
||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||
import { NonCollapsing } from '@/presentation/Scripts/Cards/NonCollapsingDirective';
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import { Component } from 'vue-property-decorator';
|
||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||
import SelectableOption from './SelectableOption.vue';
|
||||
import { IApplicationState } from '@/application/State/IApplicationState';
|
||||
|
||||
@@ -7,25 +7,37 @@
|
||||
v-show="!this.isSearching" />
|
||||
</div>
|
||||
<div class="scripts">
|
||||
<div v-if="!isSearching || searchHasMatches">
|
||||
<CardList v-if="this.showCards" />
|
||||
<div v-else-if="this.showList" class="tree">
|
||||
<div v-if="this.isSearching" class="search-query">
|
||||
Searching for "{{this.searchQuery | threeDotsTrim}}"</div>
|
||||
<ScriptsTree />
|
||||
<div v-if="!isSearching">
|
||||
<CardList v-if="currentGrouping === Grouping.Cards"/>
|
||||
<div class="tree" v-if="currentGrouping === Grouping.None">
|
||||
<ScriptsTree />
|
||||
</div>
|
||||
</div>
|
||||
<div v-else> <!-- Searching -->
|
||||
<div class="search">
|
||||
<div class="search__query">
|
||||
<div>Searching for "{{this.searchQuery | threeDotsTrim}}"</div>
|
||||
<div class="search__query__close-button">
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'times']"
|
||||
v-on:click="clearSearchQueryAsync()"/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!searchHasMatches" class="search-no-matches">
|
||||
<div>Sorry, no matches for "{{this.searchQuery | threeDotsTrim}}" 😞</div>
|
||||
<div>Feel free to extend the scripts <a :href="repositoryUrl" target="_blank" class="child github" >here</a> ✨</div>
|
||||
</div>
|
||||
<div v-else class="search-no-matches">
|
||||
Sorry, no matches for "{{this.searchQuery | threeDotsTrim}}" 😞
|
||||
Feel free to extend the scripts <a :href="repositoryUrl" target="_blank" class="child github" >here</a>.
|
||||
</div>
|
||||
<div v-if="searchHasMatches" class="tree tree--searching">
|
||||
<ScriptsTree />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue, Emit } from 'vue-property-decorator';
|
||||
import { Category } from '@/domain/Category';
|
||||
import { Component } from 'vue-property-decorator';
|
||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||
import { Grouping } from './Grouping/Grouping';
|
||||
import { IFilterResult } from '@/application/State/Filter/IFilterResult';
|
||||
@@ -34,7 +46,6 @@
|
||||
import ScriptsTree from '@/presentation/Scripts/ScriptsTree/ScriptsTree.vue';
|
||||
import CardList from '@/presentation/Scripts/Cards/CardList.vue';
|
||||
|
||||
|
||||
/** Shows content of single category or many categories */
|
||||
@Component({
|
||||
components: {
|
||||
@@ -54,38 +65,34 @@
|
||||
},
|
||||
})
|
||||
export default class TheScripts extends StatefulVue {
|
||||
public showCards = false;
|
||||
public showList = false;
|
||||
public repositoryUrl = '';
|
||||
private searchQuery = '';
|
||||
private isSearching = false;
|
||||
private searchHasMatches = false;
|
||||
public Grouping = Grouping; // Make it accessible from view
|
||||
public currentGrouping = Grouping.Cards;
|
||||
public searchQuery = '';
|
||||
public isSearching = false;
|
||||
public searchHasMatches = false;
|
||||
|
||||
private currentGrouping: Grouping;
|
||||
|
||||
public async mounted() {
|
||||
const state = await this.getCurrentStateAsync();
|
||||
this.repositoryUrl = state.app.repositoryUrl;
|
||||
state.filter.filterRemoved.on(() => {
|
||||
this.isSearching = false;
|
||||
this.updateGroups();
|
||||
});
|
||||
state.filter.filtered.on((result: IFilterResult) => {
|
||||
this.searchQuery = result.query;
|
||||
this.isSearching = true;
|
||||
this.searchHasMatches = result.hasAnyMatches();
|
||||
this.updateGroups();
|
||||
});
|
||||
}
|
||||
|
||||
public async clearSearchQueryAsync() {
|
||||
const state = await this.getCurrentStateAsync();
|
||||
state.filter.removeFilter();
|
||||
}
|
||||
|
||||
public onGroupingChanged(group: Grouping) {
|
||||
this.currentGrouping = group;
|
||||
this.updateGroups();
|
||||
}
|
||||
|
||||
private updateGroups(): void {
|
||||
this.showCards = !this.isSearching && this.currentGrouping === Grouping.Cards;
|
||||
this.showList = this.isSearching || this.currentGrouping === Grouping.None;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -95,26 +102,49 @@
|
||||
@import "@/presentation/styles/fonts.scss";
|
||||
.scripts {
|
||||
margin-top:10px;
|
||||
.search-no-matches {
|
||||
word-break:break-word;
|
||||
color: $white;
|
||||
text-transform: uppercase;
|
||||
color: $light-gray;
|
||||
font-size: 1.5em;
|
||||
background-color: $slate;
|
||||
padding:5%;
|
||||
text-align:center;
|
||||
> a {
|
||||
color: $gray;
|
||||
}
|
||||
}
|
||||
.tree {
|
||||
padding-left: 3%;
|
||||
padding-top: 15px;
|
||||
padding-bottom: 15px;
|
||||
.search-query {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
&--searching {
|
||||
padding-top: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: $slate;
|
||||
&__query {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-top: 1em;
|
||||
color: $gray;
|
||||
&__close-button {
|
||||
cursor: pointer;
|
||||
font-size: 1.25em;
|
||||
margin-left: 0.25rem;
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
}
|
||||
&-no-matches {
|
||||
display:flex;
|
||||
flex-direction: column;
|
||||
word-break:break-word;
|
||||
text-transform: uppercase;
|
||||
color: $light-gray;
|
||||
font-size: 1.5em;
|
||||
padding:10px;
|
||||
text-align:center;
|
||||
> div {
|
||||
padding-bottom:13px;
|
||||
}
|
||||
a {
|
||||
color: $gray;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Watch, Vue } from 'vue-property-decorator';
|
||||
import { Component, Prop } from 'vue-property-decorator';
|
||||
import { StatefulVue } from './StatefulVue';
|
||||
import ace from 'ace-builds';
|
||||
import 'ace-builds/webpack-resolver';
|
||||
import { CodeBuilder } from '../application/State/Code/CodeBuilder';
|
||||
import { CodeBuilder } from '@/application/State/Code/Generation/CodeBuilder';
|
||||
import { ICodeChangedEvent } from '@/application/State/Code/Event/ICodeChangedEvent';
|
||||
import { IScript } from '@/domain/IScript';
|
||||
|
||||
const NothingChosenCode =
|
||||
new CodeBuilder()
|
||||
@@ -28,19 +30,65 @@ const NothingChosenCode =
|
||||
@Component
|
||||
export default class TheCodeArea extends StatefulVue {
|
||||
public readonly editorId = 'codeEditor';
|
||||
|
||||
private editor!: ace.Ace.Editor;
|
||||
private currentMarkerId?: number;
|
||||
|
||||
@Prop() private theme!: string;
|
||||
|
||||
public async mounted() {
|
||||
this.editor = initializeEditor(this.theme, this.editorId);
|
||||
const state = await this.getCurrentStateAsync();
|
||||
this.updateCode(state.code.current);
|
||||
this.editor.setValue(state.code.current || NothingChosenCode, 1);
|
||||
state.code.changed.on((code) => this.updateCode(code));
|
||||
}
|
||||
|
||||
private updateCode(code: string) {
|
||||
this.editor.setValue(code || NothingChosenCode, 1);
|
||||
private updateCode(event: ICodeChangedEvent) {
|
||||
this.removeCurrentHighlighting();
|
||||
if (event.isEmpty()) {
|
||||
this.editor.setValue(NothingChosenCode, 1);
|
||||
return;
|
||||
}
|
||||
this.editor.setValue(event.code, 1);
|
||||
|
||||
if (event.addedScripts && event.addedScripts.length) {
|
||||
this.reactToChanges(event, event.addedScripts);
|
||||
} else if (event.changedScripts && event.changedScripts.length) {
|
||||
this.reactToChanges(event, event.changedScripts);
|
||||
}
|
||||
}
|
||||
|
||||
private reactToChanges(event: ICodeChangedEvent, scripts: ReadonlyArray<IScript>) {
|
||||
const positions = scripts
|
||||
.map((script) => event.getScriptPositionInCode(script));
|
||||
const start = Math.min(
|
||||
...positions.map((position) => position.startLine),
|
||||
);
|
||||
const end = Math.max(
|
||||
...positions.map((position) => position.endLine),
|
||||
);
|
||||
this.scrollToLine(end + 2);
|
||||
this.highlight(start, end);
|
||||
}
|
||||
|
||||
private highlight(startRow: number, endRow: number) {
|
||||
const AceRange = ace.require('ace/range').Range;
|
||||
this.currentMarkerId = this.editor.session.addMarker(
|
||||
new AceRange(startRow, 0, endRow, 0), 'code-area__highlight', 'fullLine',
|
||||
);
|
||||
}
|
||||
|
||||
private scrollToLine(row: number) {
|
||||
const column = this.editor.session.getLine(row).length;
|
||||
this.editor.gotoLine(row, column, true);
|
||||
}
|
||||
|
||||
private removeCurrentHighlighting() {
|
||||
if (!this.currentMarkerId) {
|
||||
return;
|
||||
}
|
||||
this.editor.session.removeMarker(this.currentMarkerId);
|
||||
this.currentMarkerId = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,12 +106,16 @@ function initializeEditor(theme: string, editorId: string): ace.Ace.Editor {
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
<style lang="scss">
|
||||
@import "@/presentation/styles/colors.scss";
|
||||
.code-area {
|
||||
/* ----- Fill its parent div ------ */
|
||||
width: 100%;
|
||||
/* height */
|
||||
max-height: 1000px;
|
||||
min-height: 200px;
|
||||
&__highlight {
|
||||
background-color:$accent;
|
||||
opacity: 20%;
|
||||
position:absolute;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -14,13 +14,12 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import { Component } from 'vue-property-decorator';
|
||||
import { StatefulVue } from './StatefulVue';
|
||||
import { SaveFileDialog } from './../infrastructure/SaveFileDialog';
|
||||
import { Clipboard } from './../infrastructure/Clipboard';
|
||||
import IconButton from './IconButton.vue';
|
||||
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
IconButton,
|
||||
@@ -33,7 +32,7 @@ export default class TheCodeButtons extends StatefulVue {
|
||||
const state = await this.getCurrentStateAsync();
|
||||
this.hasCode = state.code.current && state.code.current.length > 0;
|
||||
state.code.changed.on((code) => {
|
||||
this.hasCode = code && code.length > 0;
|
||||
this.hasCode = code && code.code.length > 0;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
import { Environment } from '@/application/Environment/Environment';
|
||||
import { OperatingSystem } from '@/application/Environment/OperatingSystem';
|
||||
import DownloadUrlListItem from './DownloadUrlListItem.vue';
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
|
||||
import { Component, Prop, Watch } from 'vue-property-decorator';
|
||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||
import { Environment } from '@/application/Environment/Environment';
|
||||
import { OperatingSystem } from '@/application/Environment/OperatingSystem';
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import { Component } from 'vue-property-decorator';
|
||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||
import { Environment } from '@/application/Environment/Environment';
|
||||
|
||||
|
||||
@@ -47,12 +47,11 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import { Component } from 'vue-property-decorator';
|
||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||
import { Environment } from '@/application/Environment/Environment';
|
||||
import PrivacyPolicy from './PrivacyPolicy.vue';
|
||||
import DownloadUrlList from './DownloadUrlList.vue';
|
||||
import { OperatingSystem } from '@/application/Environment/OperatingSystem';
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import { Component } from 'vue-property-decorator';
|
||||
import { StatefulVue } from './StatefulVue';
|
||||
|
||||
@Component
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="search" v-non-collapsing>
|
||||
<input type="search" class="searchTerm"
|
||||
:placeholder="searchPlaceHolder"
|
||||
@input="updateFilterAsync($event.target.value)" >
|
||||
v-model="searchQuery" >
|
||||
<div class="iconWrapper">
|
||||
<font-awesome-icon :icon="['fas', 'search']" />
|
||||
</div>
|
||||
@@ -10,9 +10,10 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
|
||||
import { Component, Watch } from 'vue-property-decorator';
|
||||
import { StatefulVue } from './StatefulVue';
|
||||
import { NonCollapsing } from '@/presentation/Scripts/Cards/NonCollapsingDirective';
|
||||
import { IUserFilter } from '@/application/State/IApplicationState';
|
||||
|
||||
@Component( {
|
||||
directives: { NonCollapsing },
|
||||
@@ -20,14 +21,16 @@ import { NonCollapsing } from '@/presentation/Scripts/Cards/NonCollapsingDirecti
|
||||
)
|
||||
export default class TheSearchBar extends StatefulVue {
|
||||
public searchPlaceHolder = 'Search';
|
||||
public searchQuery = '';
|
||||
|
||||
public async mounted() {
|
||||
const state = await this.getCurrentStateAsync();
|
||||
const totalScripts = state.app.totalScripts;
|
||||
const totalCategories = state.app.totalCategories;
|
||||
this.searchPlaceHolder = `Search in ${totalScripts} scripts`;
|
||||
this.beginReacting(state.filter);
|
||||
}
|
||||
|
||||
@Watch('searchQuery')
|
||||
public async updateFilterAsync(filter: |string) {
|
||||
const state = await this.getCurrentStateAsync();
|
||||
if (!filter) {
|
||||
@@ -37,6 +40,10 @@ export default class TheSearchBar extends StatefulVue {
|
||||
}
|
||||
}
|
||||
|
||||
private beginReacting(filter: IUserFilter) {
|
||||
filter.filtered.on((result) => this.searchQuery = result.query);
|
||||
filter.filterRemoved.on(() => this.searchQuery = '');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -80,5 +87,4 @@ export default class TheSearchBar extends StatefulVue {
|
||||
font-size: 20px;
|
||||
padding:5px;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import { CategoryStub } from './../../../stubs/CategoryStub';
|
||||
import { ScriptStub } from './../../../stubs/ScriptStub';
|
||||
import { ApplicationStub } from './../../../stubs/ApplicationStub';
|
||||
import { UserSelection } from '@/application/State/Selection/UserSelection';
|
||||
import { ApplicationCode } from '@/application/State/Code/ApplicationCode';
|
||||
import 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
||||
import { ICodeChangedEvent } from '@/application/State/Code/Event/ICodeChangedEvent';
|
||||
import { IUserScriptGenerator } from '@/application/State/Code/Generation/IUserScriptGenerator';
|
||||
import { CodePosition } from '@/application/State/Code/Position/CodePosition';
|
||||
import { ICodePosition } from '@/application/State/Code/Position/ICodePosition';
|
||||
|
||||
describe('ApplicationCode', () => {
|
||||
describe('ctor', () => {
|
||||
@@ -22,7 +26,7 @@ describe('ApplicationCode', () => {
|
||||
// arrange
|
||||
const scripts = [new ScriptStub('first'), new ScriptStub('second')];
|
||||
const app = new ApplicationStub().withAction(new CategoryStub(1).withScripts(...scripts));
|
||||
const selection = new UserSelection(app, scripts);
|
||||
const selection = new UserSelection(app, scripts.map((script) => new SelectedScript(script, false)));
|
||||
const version = 'version-string';
|
||||
const sut = new ApplicationCode(selection, version);
|
||||
// act
|
||||
@@ -31,35 +35,76 @@ describe('ApplicationCode', () => {
|
||||
expect(actual).to.have.length.greaterThan(0).and.include(version);
|
||||
});
|
||||
});
|
||||
describe('user selection changes', () => {
|
||||
it('empty when selection is empty', () => {
|
||||
// arrange
|
||||
let signaled: string;
|
||||
const scripts = [new ScriptStub('first'), new ScriptStub('second')];
|
||||
const app = new ApplicationStub().withAction(new CategoryStub(1).withScripts(...scripts));
|
||||
const selection = new UserSelection(app, scripts);
|
||||
const sut = new ApplicationCode(selection, 'version');
|
||||
sut.changed.on((code) => signaled = code);
|
||||
// act
|
||||
selection.changed.notify([]);
|
||||
// assert
|
||||
expect(signaled).to.have.lengthOf(0);
|
||||
expect(signaled).to.equal(sut.current);
|
||||
describe('changed event', () => {
|
||||
describe('code', () => {
|
||||
it('empty when nothing is selected', () => {
|
||||
// arrange
|
||||
let signaled: ICodeChangedEvent;
|
||||
const scripts = [new ScriptStub('first'), new ScriptStub('second')];
|
||||
const app = new ApplicationStub().withAction(new CategoryStub(1).withScripts(...scripts));
|
||||
const selection = new UserSelection(app, scripts.map((script) => new SelectedScript(script, false)));
|
||||
const sut = new ApplicationCode(selection, 'version');
|
||||
sut.changed.on((code) => signaled = code);
|
||||
// act
|
||||
selection.changed.notify([]);
|
||||
// assert
|
||||
expect(signaled.code).to.have.lengthOf(0);
|
||||
expect(signaled.code).to.equal(sut.current);
|
||||
});
|
||||
it('has code when some are selected', () => {
|
||||
// arrange
|
||||
let signaled: ICodeChangedEvent;
|
||||
const scripts = [new ScriptStub('first'), new ScriptStub('second')];
|
||||
const app = new ApplicationStub().withAction(new CategoryStub(1).withScripts(...scripts));
|
||||
const selection = new UserSelection(app, scripts.map((script) => new SelectedScript(script, false)));
|
||||
const version = 'version-string';
|
||||
const sut = new ApplicationCode(selection, version);
|
||||
sut.changed.on((code) => signaled = code);
|
||||
// act
|
||||
selection.changed.notify(scripts.map((s) => new SelectedScript(s, false)));
|
||||
// assert
|
||||
expect(signaled.code).to.have.length.greaterThan(0).and.include(version);
|
||||
expect(signaled.code).to.equal(sut.current);
|
||||
});
|
||||
});
|
||||
it('has code when selection is not empty', () => {
|
||||
it('sets positions from the generator', () => {
|
||||
// arrange
|
||||
let signaled: string;
|
||||
let signaled: ICodeChangedEvent;
|
||||
const scripts = [new ScriptStub('first'), new ScriptStub('second')];
|
||||
const app = new ApplicationStub().withAction(new CategoryStub(1).withScripts(...scripts));
|
||||
const selection = new UserSelection(app, scripts);
|
||||
const version = 'version-string';
|
||||
const sut = new ApplicationCode(selection, version);
|
||||
const selection = new UserSelection(app, scripts.map((script) => new SelectedScript(script, false)));
|
||||
const expectedVersion = 'version-string';
|
||||
const scriptsToSelect = scripts.map((s) => new SelectedScript(s, false));
|
||||
const totalLines = 20;
|
||||
const expected = new Map<SelectedScript, ICodePosition>(
|
||||
[
|
||||
[ scriptsToSelect[0], new CodePosition(0, totalLines / 2)],
|
||||
[ scriptsToSelect[1], new CodePosition(totalLines / 2, totalLines)],
|
||||
],
|
||||
);
|
||||
const generatorMock: IUserScriptGenerator = {
|
||||
buildCode: (selectedScripts, version) => {
|
||||
if (version !== expectedVersion) {
|
||||
throw new Error('Unexpected version');
|
||||
}
|
||||
if (JSON.stringify(selectedScripts) !== JSON.stringify(scriptsToSelect)) {
|
||||
throw new Error('Unexpected scripts');
|
||||
}
|
||||
return {
|
||||
code: '\nREM LINE'.repeat(totalLines),
|
||||
scriptPositions: expected,
|
||||
};
|
||||
},
|
||||
};
|
||||
const sut = new ApplicationCode(selection, expectedVersion, generatorMock);
|
||||
sut.changed.on((code) => signaled = code);
|
||||
// act
|
||||
selection.changed.notify(scripts.map((s) => new SelectedScript(s, false)));
|
||||
selection.changed.notify(scriptsToSelect);
|
||||
// assert
|
||||
expect(signaled).to.have.length.greaterThan(0).and.include(version);
|
||||
expect(signaled).to.equal(sut.current);
|
||||
expect(signaled.getScriptPositionInCode(scripts[0]))
|
||||
.to.deep.equal(expected.get(scriptsToSelect[0]));
|
||||
expect(signaled.getScriptPositionInCode(scripts[1]))
|
||||
.to.deep.equal(expected.get(scriptsToSelect[1]));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
147
tests/unit/application/State/Code/Event/CodeChangedEvent.spec.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import { CodeChangedEvent } from '@/application/State/Code/Event/CodeChangedEvent';
|
||||
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
||||
import { ICodePosition } from '@/application/State/Code/Position/ICodePosition';
|
||||
import { CodePosition } from '@/application/State/Code/Position/CodePosition';
|
||||
import { SelectedScriptStub } from '../../../../stubs/SelectedScriptStub';
|
||||
import { ScriptStub } from '../../../../stubs/ScriptStub';
|
||||
|
||||
describe('CodeChangedEvent', () => {
|
||||
describe('ctor', () => {
|
||||
describe('position validation', () => {
|
||||
it('throws when code position is out of range', () => {
|
||||
const act = () => new CodeChangedEvent(
|
||||
'singleline code', [], new Map<SelectedScript, ICodePosition>([
|
||||
[ new SelectedScriptStub('1'), new CodePosition(0, 2) ],
|
||||
]),
|
||||
);
|
||||
expect(act).to.throw();
|
||||
});
|
||||
it('does not throw with valid code position', () => {
|
||||
const act = () => new CodeChangedEvent(
|
||||
'singleline code', [], new Map<SelectedScript, ICodePosition>([
|
||||
[ new SelectedScriptStub('1'), new CodePosition(0, 1) ],
|
||||
]),
|
||||
);
|
||||
expect(act).to.not.throw();
|
||||
});
|
||||
});
|
||||
});
|
||||
it('code returns expected', () => {
|
||||
// arrange
|
||||
const expected = 'code';
|
||||
// act
|
||||
const sut = new CodeChangedEvent(
|
||||
expected, [], new Map<SelectedScript, ICodePosition>(),
|
||||
);
|
||||
const actual = sut.code;
|
||||
// assert
|
||||
expect(actual).to.equal(expected);
|
||||
});
|
||||
describe('addedScripts', () => {
|
||||
it('returns new scripts when scripts are added', () => {
|
||||
// arrange
|
||||
const expected = [ new ScriptStub('3'), new ScriptStub('4') ];
|
||||
const initialScripts = [ new SelectedScriptStub('1'), new SelectedScriptStub('2') ];
|
||||
const newScripts = new Map<SelectedScript, ICodePosition>([
|
||||
[initialScripts[0], new CodePosition(0, 1) ],
|
||||
[initialScripts[1], new CodePosition(0, 1) ],
|
||||
[new SelectedScript(expected[0], false), new CodePosition(0, 1) ],
|
||||
[new SelectedScript(expected[1], false), new CodePosition(0, 1) ],
|
||||
]);
|
||||
// act
|
||||
const sut = new CodeChangedEvent(
|
||||
'code', initialScripts, newScripts,
|
||||
);
|
||||
const actual = sut.addedScripts;
|
||||
// assert
|
||||
expect(actual).to.have.lengthOf(2);
|
||||
expect(actual[0]).to.deep.equal(expected[0]);
|
||||
expect(actual[1]).to.deep.equal(expected[1]);
|
||||
});
|
||||
});
|
||||
describe('removedScripts', () => {
|
||||
it('returns removed scripts when script are removed', () => {
|
||||
// arrange
|
||||
const existingScripts = [ new SelectedScriptStub('0'), new SelectedScriptStub('1') ];
|
||||
const removedScripts = [ new SelectedScriptStub('2') ];
|
||||
const initialScripts = [ ...existingScripts, ...removedScripts ];
|
||||
const newScripts = new Map<SelectedScript, ICodePosition>([
|
||||
[initialScripts[0], new CodePosition(0, 1) ],
|
||||
[initialScripts[1], new CodePosition(0, 1) ],
|
||||
]);
|
||||
// act
|
||||
const sut = new CodeChangedEvent(
|
||||
'code', initialScripts, newScripts,
|
||||
);
|
||||
const actual = sut.removedScripts;
|
||||
// assert
|
||||
expect(actual).to.have.lengthOf(removedScripts.length);
|
||||
expect(actual[0]).to.deep.equal(removedScripts[0].script);
|
||||
});
|
||||
});
|
||||
describe('changedScripts', () => {
|
||||
it('returns changed scripts when scripts are changed', () => {
|
||||
// arrange
|
||||
const initialScripts = [ new SelectedScriptStub('1', false), new SelectedScriptStub('2', false) ];
|
||||
const newScripts = new Map<SelectedScript, ICodePosition>([
|
||||
[new SelectedScriptStub('1', true), new CodePosition(0, 1) ],
|
||||
[new SelectedScriptStub('2', false), new CodePosition(0, 1) ],
|
||||
]);
|
||||
// act
|
||||
const sut = new CodeChangedEvent(
|
||||
'code', initialScripts, newScripts,
|
||||
);
|
||||
const actual = sut.changedScripts;
|
||||
// assert
|
||||
expect(actual).to.have.lengthOf(1);
|
||||
expect(actual[0]).to.deep.equal(initialScripts[0].script);
|
||||
});
|
||||
});
|
||||
describe('isEmpty', () => {
|
||||
it('returns true when empty', () => {
|
||||
// arrange
|
||||
const newScripts = new Map<SelectedScript, ICodePosition>();
|
||||
const oldScripts = [ new SelectedScriptStub('1', false) ];
|
||||
const sut = new CodeChangedEvent(
|
||||
'code', oldScripts, newScripts,
|
||||
);
|
||||
// act
|
||||
const actual = sut.isEmpty();
|
||||
// assert
|
||||
expect(actual).to.equal(true);
|
||||
});
|
||||
it('returns false when not empty', () => {
|
||||
// arrange
|
||||
const oldScripts = [ new SelectedScriptStub('1') ];
|
||||
const newScripts = new Map<SelectedScript, ICodePosition>( [
|
||||
[oldScripts[0], new CodePosition(0, 1) ],
|
||||
]);
|
||||
const sut = new CodeChangedEvent(
|
||||
'code', oldScripts, newScripts,
|
||||
);
|
||||
// act
|
||||
const actual = sut.isEmpty();
|
||||
// assert
|
||||
expect(actual).to.equal(false);
|
||||
});
|
||||
});
|
||||
describe('getScriptPositionInCode', () => {
|
||||
it('returns expected position for existing script', () => {
|
||||
// arrange
|
||||
const script = new ScriptStub('1');
|
||||
const expected = new CodePosition(0, 1);
|
||||
const newScripts = new Map<SelectedScript, ICodePosition>( [
|
||||
[new SelectedScript(script, false), expected ],
|
||||
]);
|
||||
const sut = new CodeChangedEvent(
|
||||
'code', [], newScripts,
|
||||
);
|
||||
// act
|
||||
const actual = sut.getScriptPositionInCode(script);
|
||||
// assert
|
||||
expect(actual).to.deep.equal(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
112
tests/unit/application/State/Code/Generation/CodeBuilder.spec.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import { CodeBuilder } from '@/application/State/Code/Generation/CodeBuilder';
|
||||
|
||||
describe('CodeBuilder', () => {
|
||||
describe('appendLine', () => {
|
||||
it('when empty appends empty line', () => {
|
||||
// arrange
|
||||
const sut = new CodeBuilder();
|
||||
// act
|
||||
sut.appendLine().appendLine().appendLine();
|
||||
// assert
|
||||
expect(sut.toString()).to.equal('\n\n');
|
||||
});
|
||||
it('when not empty append string in new line', () => {
|
||||
// arrange
|
||||
const sut = new CodeBuilder();
|
||||
const expected = 'str';
|
||||
// act
|
||||
sut.appendLine()
|
||||
.appendLine(expected);
|
||||
// assert
|
||||
const result = sut.toString();
|
||||
const lines = getLines(result);
|
||||
expect(lines[1]).to.equal('str');
|
||||
});
|
||||
});
|
||||
it('appendFunction', () => {
|
||||
// arrange
|
||||
const sut = new CodeBuilder();
|
||||
const functionName = 'function';
|
||||
const code = 'code';
|
||||
// act
|
||||
sut.appendFunction(functionName, code);
|
||||
// assert
|
||||
const result = sut.toString();
|
||||
expect(result).to.include(functionName);
|
||||
expect(result).to.include(code);
|
||||
});
|
||||
it('appendTrailingHyphensCommentLine', () => {
|
||||
// arrange
|
||||
const sut = new CodeBuilder();
|
||||
const totalHypens = 5;
|
||||
const expected = `:: ${'-'.repeat(totalHypens)}`;
|
||||
// act
|
||||
sut.appendTrailingHyphensCommentLine(totalHypens);
|
||||
// assert
|
||||
const result = sut.toString();
|
||||
const lines = getLines(result);
|
||||
expect(lines[0]).to.equal(expected);
|
||||
});
|
||||
it('appendCommentLine', () => {
|
||||
// arrange
|
||||
const sut = new CodeBuilder();
|
||||
const comment = 'comment';
|
||||
const expected = ':: comment';
|
||||
// act
|
||||
sut.appendCommentLine(comment);
|
||||
// assert
|
||||
const result = sut.toString();
|
||||
const lines = getLines(result);
|
||||
expect(lines[0]).to.equal(expected);
|
||||
});
|
||||
it('appendCommentLineWithHyphensAround', () => {
|
||||
// arrange
|
||||
const sut = new CodeBuilder();
|
||||
const sectionName = 'section';
|
||||
const totalHypens = sectionName.length + 3 * 2;
|
||||
const expected = ':: ---section---';
|
||||
sut.appendCommentLineWithHyphensAround(sectionName, totalHypens);
|
||||
// assert
|
||||
const result = sut.toString();
|
||||
const lines = getLines(result);
|
||||
expect(lines[1]).to.equal(expected);
|
||||
});
|
||||
describe('currentLine', () => {
|
||||
it('no lines returns zero', () => {
|
||||
// arrange & act
|
||||
const sut = new CodeBuilder();
|
||||
// assert
|
||||
expect(sut.currentLine).to.equal(0);
|
||||
});
|
||||
it('single line returns one', () => {
|
||||
// arrange
|
||||
const sut = new CodeBuilder();
|
||||
// act
|
||||
sut.appendLine();
|
||||
// assert
|
||||
expect(sut.currentLine).to.equal(1);
|
||||
});
|
||||
it('multiple lines returns as expected', () => {
|
||||
// arrange
|
||||
const sut = new CodeBuilder();
|
||||
// act
|
||||
sut.appendLine('1').appendCommentLine('2').appendLine();
|
||||
// assert
|
||||
expect(sut.currentLine).to.equal(3);
|
||||
});
|
||||
it('multiple lines in code', () => {
|
||||
// arrange
|
||||
const sut = new CodeBuilder();
|
||||
// act
|
||||
sut.appendLine('hello\ncode-here\nwith-3-lines');
|
||||
// assert
|
||||
expect(sut.currentLine).to.equal(3);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function getLines(text: string): string[] {
|
||||
return text.split(/\r\n|\r|\n/);
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import { ScriptStub } from '../../../../stubs/ScriptStub';
|
||||
import { UserScriptGenerator, adminRightsScript } from '@/application/State/Code/Generation/UserScriptGenerator';
|
||||
import 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
||||
import { SelectedScriptStub } from '../../../../stubs/SelectedScriptStub';
|
||||
|
||||
describe('UserScriptGenerator', () => {
|
||||
it('adds version', () => {
|
||||
// arrange
|
||||
const sut = new UserScriptGenerator();
|
||||
const version = '1.5.0';
|
||||
const selectedScripts = [ new SelectedScript(new ScriptStub('id'), false)];
|
||||
// act
|
||||
const actual = sut.buildCode(selectedScripts, version);
|
||||
// assert
|
||||
expect(actual.code).to.include(version);
|
||||
});
|
||||
it('adds admin rights function', () => {
|
||||
// arrange
|
||||
const sut = new UserScriptGenerator();
|
||||
const selectedScripts = [ new SelectedScript(new ScriptStub('id'), false)];
|
||||
// act
|
||||
const actual = sut.buildCode(selectedScripts, 'non-important-version');
|
||||
// assert
|
||||
expect(actual.code).to.include(adminRightsScript.code);
|
||||
expect(actual.code).to.include(adminRightsScript.name);
|
||||
});
|
||||
it('appends revert script', () => {
|
||||
// arrange
|
||||
const sut = new UserScriptGenerator();
|
||||
const scriptName = 'test non-revert script';
|
||||
const scriptCode = 'REM nop';
|
||||
const script = new ScriptStub('id').withName(scriptName).withRevertCode(scriptCode);
|
||||
const selectedScripts = [ new SelectedScript(script, true)];
|
||||
// act
|
||||
const actual = sut.buildCode(selectedScripts, 'non-important-version');
|
||||
// assert
|
||||
expect(actual.code).to.include(`${scriptName} (revert)`);
|
||||
expect(actual.code).to.include(scriptCode);
|
||||
});
|
||||
it('appends non-revert script', () => {
|
||||
const sut = new UserScriptGenerator();
|
||||
// arrange
|
||||
const scriptName = 'test non-revert script';
|
||||
const scriptCode = 'REM nop';
|
||||
const script = new ScriptStub('id').withName(scriptName).withCode(scriptCode);
|
||||
const selectedScripts = [ new SelectedScript(script, false)];
|
||||
// act
|
||||
const actual = sut.buildCode(selectedScripts, 'non-important-version');
|
||||
// assert
|
||||
expect(actual.code).to.include(scriptName);
|
||||
expect(actual.code).to.include(scriptCode);
|
||||
});
|
||||
describe('scriptPositions', () => {
|
||||
it('single script', () => {
|
||||
// arrange
|
||||
const sut = new UserScriptGenerator();
|
||||
const scriptName = 'test non-revert script';
|
||||
const scriptCode = 'REM nop\nREM nop2';
|
||||
const script = new ScriptStub('id').withName(scriptName).withCode(scriptCode);
|
||||
const selectedScripts = [ new SelectedScript(script, false)];
|
||||
// act
|
||||
const actual = sut.buildCode(selectedScripts, 'non-important-version');
|
||||
// assert
|
||||
expect(actual.scriptPositions.size).to.equal(1);
|
||||
const position = actual.scriptPositions.get(selectedScripts[0]);
|
||||
expect(position.endLine).to.be.greaterThan(position.startLine + 2);
|
||||
});
|
||||
it('multiple scripts', () => {
|
||||
// arrange
|
||||
const sut = new UserScriptGenerator();
|
||||
const selectedScripts = [ new SelectedScriptStub('1'), new SelectedScriptStub('2') ];
|
||||
// act
|
||||
const actual = sut.buildCode(selectedScripts, 'non-important-version');
|
||||
// assert
|
||||
const firstPosition = actual.scriptPositions.get(selectedScripts[0]);
|
||||
const secondPosition = actual.scriptPositions.get(selectedScripts[1]);
|
||||
expect(actual.scriptPositions.size).to.equal(2);
|
||||
expect(firstPosition.endLine).to.be.greaterThan(firstPosition.startLine + 1);
|
||||
expect(secondPosition.startLine).to.be.greaterThan(firstPosition.endLine);
|
||||
expect(secondPosition.endLine).to.be.greaterThan(secondPosition.startLine + 1);
|
||||
});
|
||||
it('no script', () => {
|
||||
// arrange
|
||||
const sut = new UserScriptGenerator();
|
||||
const selectedScripts = [ ];
|
||||
// act
|
||||
const actual = sut.buildCode(selectedScripts, 'non-important-version');
|
||||
// assert
|
||||
expect(actual.scriptPositions.size).to.equal(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
import { CodePosition } from '@/application/State/Code/Position/CodePosition';
|
||||
import 'mocha';
|
||||
import { expect } from 'chai';
|
||||
|
||||
describe('CodePosition', () => {
|
||||
describe('ctor', () => {
|
||||
it('creates with valid parameters', () => {
|
||||
// arrange
|
||||
const startPosition = 0;
|
||||
const endPosition = 5;
|
||||
// act
|
||||
const sut = new CodePosition(startPosition, endPosition);
|
||||
// assert
|
||||
expect(sut.startLine).to.equal(startPosition);
|
||||
expect(sut.endLine).to.equal(endPosition);
|
||||
});
|
||||
it('throws with negative start position', () => {
|
||||
// arrange
|
||||
const startPosition = -1;
|
||||
const endPosition = 5;
|
||||
// act
|
||||
const getSut = () => new CodePosition(startPosition, endPosition);
|
||||
// assert
|
||||
expect(getSut).to.throw('Code cannot start in a negative line');
|
||||
});
|
||||
it('throws with negative end position', () => {
|
||||
// arrange
|
||||
const startPosition = 1;
|
||||
const endPosition = -5;
|
||||
// act
|
||||
const getSut = () => new CodePosition(startPosition, endPosition);
|
||||
// assert
|
||||
expect(getSut).to.throw('Code cannot end in a negative line');
|
||||
});
|
||||
it('throws when start and end position is same', () => {
|
||||
// arrange
|
||||
const startPosition = 0;
|
||||
const endPosition = 0;
|
||||
// act
|
||||
const getSut = () => new CodePosition(startPosition, endPosition);
|
||||
// assert
|
||||
expect(getSut).to.throw('Empty code');
|
||||
});
|
||||
it('throws when ends before start', () => {
|
||||
// arrange
|
||||
const startPosition = 3;
|
||||
const endPosition = 2;
|
||||
// act
|
||||
const getSut = () => new CodePosition(startPosition, endPosition);
|
||||
// assert
|
||||
expect(getSut).to.throw('End line cannot be less than start line');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,54 +0,0 @@
|
||||
import { ScriptStub } from './../../../stubs/ScriptStub';
|
||||
import { UserScriptGenerator, adminRightsScript } from '@/application/State/Code/UserScriptGenerator';
|
||||
import 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
||||
|
||||
describe('UserScriptGenerator', () => {
|
||||
it('adds version', () => {
|
||||
const sut = new UserScriptGenerator();
|
||||
// arrange
|
||||
const version = '1.5.0';
|
||||
const selectedScripts = [ new SelectedScript(new ScriptStub('id'), false)];
|
||||
// act
|
||||
const actual = sut.buildCode(selectedScripts, version);
|
||||
// assert
|
||||
expect(actual).to.include(version);
|
||||
});
|
||||
it('adds admin rights function', () => {
|
||||
const sut = new UserScriptGenerator();
|
||||
// arrange
|
||||
const selectedScripts = [ new SelectedScript(new ScriptStub('id'), false)];
|
||||
// act
|
||||
const actual = sut.buildCode(selectedScripts, 'non-important-version');
|
||||
// assert
|
||||
expect(actual).to.include(adminRightsScript.code);
|
||||
expect(actual).to.include(adminRightsScript.name);
|
||||
});
|
||||
it('appends revert script', () => {
|
||||
const sut = new UserScriptGenerator();
|
||||
// arrange
|
||||
const scriptName = 'test non-revert script';
|
||||
const scriptCode = 'REM nop';
|
||||
const script = new ScriptStub('id').withName(scriptName).withRevertCode(scriptCode);
|
||||
const selectedScripts = [ new SelectedScript(script, true)];
|
||||
// act
|
||||
const actual = sut.buildCode(selectedScripts, 'non-important-version');
|
||||
// assert
|
||||
expect(actual).to.include(`${scriptName} (revert)`);
|
||||
expect(actual).to.include(scriptCode);
|
||||
});
|
||||
it('appends non-revert script', () => {
|
||||
const sut = new UserScriptGenerator();
|
||||
// arrange
|
||||
const scriptName = 'test non-revert script';
|
||||
const scriptCode = 'REM nop';
|
||||
const script = new ScriptStub('id').withName(scriptName).withCode(scriptCode);
|
||||
const selectedScripts = [ new SelectedScript(script, false)];
|
||||
// act
|
||||
const actual = sut.buildCode(selectedScripts, 'non-important-version');
|
||||
// assert
|
||||
expect(actual).to.include(scriptName);
|
||||
expect(actual).to.include(scriptCode);
|
||||
});
|
||||
});
|
||||
@@ -1,20 +1,47 @@
|
||||
import { ScriptStub } from './../../../stubs/ScriptStub';
|
||||
import 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import { IScript } from '@/domain/IScript';
|
||||
import { SelectedScriptStub } from '../../../stubs/SelectedScriptStub';
|
||||
import { ScriptStub } from '../../../stubs/ScriptStub';
|
||||
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
||||
import { CategoryStub } from '../../../stubs/CategoryStub';
|
||||
import { ApplicationStub } from '../../../stubs/ApplicationStub';
|
||||
import { UserSelection } from '@/application/State/Selection/UserSelection';
|
||||
import 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import { IScript } from '@/domain/IScript';
|
||||
|
||||
describe('UserSelection', () => {
|
||||
describe('ctor', () => {
|
||||
it('has nothing with no initial selection', () => {
|
||||
// arrange
|
||||
const app = new ApplicationStub().withAction(new CategoryStub(1).withScriptIds('s1'));
|
||||
const selection = [];
|
||||
// act
|
||||
const sut = new UserSelection(app, selection);
|
||||
// assert
|
||||
expect(sut.selectedScripts).to.have.lengthOf(0);
|
||||
});
|
||||
it('has initial selection', () => {
|
||||
// arrange
|
||||
const firstScript = new ScriptStub('1');
|
||||
const secondScript = new ScriptStub('2');
|
||||
const app = new ApplicationStub().withAction(
|
||||
new CategoryStub(1).withScript(firstScript).withScripts(secondScript));
|
||||
const expected = [ new SelectedScript(firstScript, false), new SelectedScript(secondScript, true) ];
|
||||
// act
|
||||
const sut = new UserSelection(app, expected);
|
||||
// assert
|
||||
expect(sut.selectedScripts).to.deep.include(expected[0]);
|
||||
expect(sut.selectedScripts).to.deep.include(expected[1]);
|
||||
});
|
||||
});
|
||||
it('deselectAll removes all items', () => {
|
||||
// arrange
|
||||
const events: Array<readonly SelectedScript[]> = [];
|
||||
const app = new ApplicationStub()
|
||||
.withAction(new CategoryStub(1)
|
||||
.withScriptIds('s1', 's2', 's3', 's4'));
|
||||
const selectedScripts = [new ScriptStub('s1'), new ScriptStub('s2'), new ScriptStub('s3')];
|
||||
const selectedScripts = [
|
||||
new SelectedScriptStub('s1'), new SelectedScriptStub('s2'), new SelectedScriptStub('s3'),
|
||||
];
|
||||
const sut = new UserSelection(app, selectedScripts);
|
||||
sut.changed.on((newScripts) => events.push(newScripts));
|
||||
// act
|
||||
@@ -30,15 +57,20 @@ describe('UserSelection', () => {
|
||||
const app = new ApplicationStub()
|
||||
.withAction(new CategoryStub(1)
|
||||
.withScriptIds('s1', 's2', 's3', 's4'));
|
||||
const selectedScripts = [new ScriptStub('s1'), new ScriptStub('s2'), new ScriptStub('s3')];
|
||||
const selectedScripts = [
|
||||
new SelectedScriptStub('s1'), new SelectedScriptStub('s2'), new SelectedScriptStub('s3'),
|
||||
];
|
||||
const sut = new UserSelection(app, selectedScripts);
|
||||
sut.changed.on((newScripts) => events.push(newScripts));
|
||||
const scripts = [new ScriptStub('s2'), new ScriptStub('s3'), new ScriptStub('s4')];
|
||||
const expected = scripts.map((script) => new SelectedScript(script, false));
|
||||
const expected = [ new SelectedScriptStub('s2'), new SelectedScriptStub('s3'),
|
||||
new SelectedScript(scripts[2], false)];
|
||||
// act
|
||||
sut.selectOnly(scripts);
|
||||
// assert
|
||||
expect(sut.selectedScripts).to.deep.equal(expected);
|
||||
expect(sut.selectedScripts).to.have.deep.members(expected,
|
||||
`Expected: ${JSON.stringify(sut.selectedScripts)}\n` +
|
||||
`Actual: ${JSON.stringify(expected)}`);
|
||||
expect(events).to.have.lengthOf(1);
|
||||
expect(events[0]).to.deep.equal(expected);
|
||||
});
|
||||
@@ -93,4 +125,171 @@ describe('UserSelection', () => {
|
||||
expect(events[0]).to.deep.equal(expected);
|
||||
});
|
||||
});
|
||||
describe('removeAllInCategory', () => {
|
||||
it('does nothing when nothing exists', () => {
|
||||
// arrange
|
||||
const events: Array<readonly SelectedScript[]> = [];
|
||||
const categoryId = 1;
|
||||
const app = new ApplicationStub()
|
||||
.withAction(new CategoryStub(categoryId)
|
||||
.withScripts(new ScriptStub('s1'), new ScriptStub('s2')));
|
||||
const sut = new UserSelection(app, []);
|
||||
sut.changed.on((s) => events.push(s));
|
||||
// act
|
||||
sut.removeAllInCategory(categoryId);
|
||||
// assert
|
||||
expect(events).to.have.lengthOf(0);
|
||||
expect(sut.selectedScripts).to.have.lengthOf(0);
|
||||
});
|
||||
it('removes all when all exists', () => {
|
||||
// arrange
|
||||
const categoryId = 1;
|
||||
const scripts = [new SelectedScriptStub('s1'), new SelectedScriptStub('s2')];
|
||||
const app = new ApplicationStub()
|
||||
.withAction(new CategoryStub(categoryId)
|
||||
.withScripts(...scripts.map((script) => script.script)));
|
||||
const sut = new UserSelection(app, scripts);
|
||||
// act
|
||||
sut.removeAllInCategory(categoryId);
|
||||
// assert
|
||||
expect(sut.totalSelected).to.equal(0);
|
||||
expect(sut.selectedScripts.length).to.equal(0);
|
||||
});
|
||||
it('removes existing some exists', () => {
|
||||
// arrange
|
||||
const categoryId = 1;
|
||||
const existing = [new ScriptStub('s1'), new ScriptStub('s2')];
|
||||
const notExisting = [new ScriptStub('s3'), new ScriptStub('s4')];
|
||||
const app = new ApplicationStub()
|
||||
.withAction(new CategoryStub(categoryId)
|
||||
.withScripts(...existing, ...notExisting));
|
||||
const sut = new UserSelection(app, existing.map((script) => new SelectedScript(script, false)));
|
||||
// act
|
||||
sut.removeAllInCategory(categoryId);
|
||||
// assert
|
||||
expect(sut.totalSelected).to.equal(0);
|
||||
expect(sut.selectedScripts.length).to.equal(0);
|
||||
});
|
||||
});
|
||||
describe('addOrUpdateAllInCategory', () => {
|
||||
it('does nothing when all already exists', () => {
|
||||
// arrange
|
||||
const events: Array<readonly SelectedScript[]> = [];
|
||||
const categoryId = 1;
|
||||
const scripts = [new ScriptStub('s1'), new ScriptStub('s2')];
|
||||
const app = new ApplicationStub()
|
||||
.withAction(new CategoryStub(categoryId)
|
||||
.withScripts(...scripts));
|
||||
const sut = new UserSelection(app, scripts.map((script) => new SelectedScript(script, false)));
|
||||
sut.changed.on((s) => events.push(s));
|
||||
// act
|
||||
sut.addOrUpdateAllInCategory(categoryId);
|
||||
// assert
|
||||
expect(events).to.have.lengthOf(0);
|
||||
expect(sut.selectedScripts.map((script) => script.id))
|
||||
.to.have.deep.members(scripts.map((script) => script.id));
|
||||
});
|
||||
it('adds all when nothing exists', () => {
|
||||
// arrange
|
||||
const categoryId = 1;
|
||||
const expected = [new ScriptStub('s1'), new ScriptStub('s2')];
|
||||
const app = new ApplicationStub()
|
||||
.withAction(new CategoryStub(categoryId)
|
||||
.withScripts(...expected));
|
||||
const sut = new UserSelection(app, []);
|
||||
// act
|
||||
sut.addOrUpdateAllInCategory(categoryId);
|
||||
// assert
|
||||
expect(sut.selectedScripts.map((script) => script.id))
|
||||
.to.have.deep.members(expected.map((script) => script.id));
|
||||
});
|
||||
it('adds all with given revert status when nothing exists', () => {
|
||||
// arrange
|
||||
const categoryId = 1;
|
||||
const expected = [new ScriptStub('s1'), new ScriptStub('s2')];
|
||||
const app = new ApplicationStub()
|
||||
.withAction(new CategoryStub(categoryId)
|
||||
.withScripts(...expected));
|
||||
const sut = new UserSelection(app, []);
|
||||
// act
|
||||
sut.addOrUpdateAllInCategory(categoryId, true);
|
||||
// assert
|
||||
expect(sut.selectedScripts.every((script) => script.revert))
|
||||
.to.equal(true);
|
||||
});
|
||||
it('changes revert status of all when some exists', () => {
|
||||
// arrange
|
||||
const categoryId = 1;
|
||||
const notExisting = [ new ScriptStub('notExisting1'), new ScriptStub('notExisting2') ];
|
||||
const existing = [ new ScriptStub('existing1'), new ScriptStub('existing2') ];
|
||||
const allScripts = [ ...existing, ...notExisting ];
|
||||
const app = new ApplicationStub()
|
||||
.withAction(new CategoryStub(categoryId)
|
||||
.withScripts(...allScripts));
|
||||
const sut = new UserSelection(app, existing.map((script) => new SelectedScript(script, false)));
|
||||
// act
|
||||
sut.addOrUpdateAllInCategory(categoryId, true);
|
||||
// assert
|
||||
expect(sut.selectedScripts.every((script) => script.revert))
|
||||
.to.equal(true);
|
||||
});
|
||||
it('changes revert status of all when some exists', () => {
|
||||
// arrange
|
||||
const categoryId = 1;
|
||||
const notExisting = [ new ScriptStub('notExisting1'), new ScriptStub('notExisting2') ];
|
||||
const existing = [ new ScriptStub('existing1'), new ScriptStub('existing2') ];
|
||||
const allScripts = [ ...existing, ...notExisting ];
|
||||
const app = new ApplicationStub()
|
||||
.withAction(new CategoryStub(categoryId)
|
||||
.withScripts(...allScripts));
|
||||
const sut = new UserSelection(app, existing.map((script) => new SelectedScript(script, false)));
|
||||
// act
|
||||
sut.addOrUpdateAllInCategory(categoryId, true);
|
||||
// assert
|
||||
expect(sut.selectedScripts.every((script) => script.revert))
|
||||
.to.equal(true);
|
||||
});
|
||||
it('changes revert status of all when all already exists', () => {
|
||||
// arrange
|
||||
const categoryId = 1;
|
||||
const scripts = [ new ScriptStub('existing1'), new ScriptStub('existing2') ];
|
||||
const app = new ApplicationStub()
|
||||
.withAction(new CategoryStub(categoryId)
|
||||
.withScripts(...scripts));
|
||||
const sut = new UserSelection(app, scripts.map((script) => new SelectedScript(script, false)));
|
||||
// act
|
||||
sut.addOrUpdateAllInCategory(categoryId, true);
|
||||
// assert
|
||||
expect(sut.selectedScripts.every((script) => script.revert))
|
||||
.to.equal(true);
|
||||
});
|
||||
});
|
||||
describe('isSelected', () => {
|
||||
it('returns false when not selected', () => {
|
||||
// arrange
|
||||
const selectedScript = new ScriptStub('selected');
|
||||
const notSelectedScript = new ScriptStub('not selected');
|
||||
const app = new ApplicationStub()
|
||||
.withAction(new CategoryStub(1)
|
||||
.withScripts(selectedScript, notSelectedScript));
|
||||
const sut = new UserSelection(app, [ new SelectedScript(selectedScript, false) ]);
|
||||
// act
|
||||
const actual = sut.isSelected(notSelectedScript.id);
|
||||
// assert
|
||||
expect(actual).to.equal(false);
|
||||
});
|
||||
it('returns true when selected', () => {
|
||||
// arrange
|
||||
const selectedScript = new ScriptStub('selected');
|
||||
const notSelectedScript = new ScriptStub('not selected');
|
||||
const app = new ApplicationStub()
|
||||
.withAction(new CategoryStub(1)
|
||||
.withScripts(selectedScript, notSelectedScript));
|
||||
const sut = new UserSelection(app, [ new SelectedScript(selectedScript, false) ]);
|
||||
// act
|
||||
const actual = sut.isSelected(selectedScript.id);
|
||||
// assert
|
||||
expect(actual).to.equal(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
89
tests/unit/domain/Category.spec.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import { Category } from '@/domain/Category';
|
||||
import { CategoryStub } from '../stubs/CategoryStub';
|
||||
import { ScriptStub } from '../stubs/ScriptStub';
|
||||
|
||||
describe('Category', () => {
|
||||
describe('ctor', () => {
|
||||
it('throws when name is empty', () => {
|
||||
const expectedError = 'undefined or empty name';
|
||||
const construct = () => new Category(5, '', [], [new CategoryStub(5)], []);
|
||||
expect(construct).to.throw(expectedError);
|
||||
});
|
||||
it('throws when has no children', () => {
|
||||
const expectedError = 'A category must have at least one sub-category or script';
|
||||
const construct = () => new Category(5, 'category', [], [], []);
|
||||
expect(construct).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
describe('getAllScriptsRecursively', () => {
|
||||
it('gets child scripts', () => {
|
||||
// arrange
|
||||
const expected = [ new ScriptStub('1'), new ScriptStub('2') ];
|
||||
const sut = new Category(0, 'category', [], [], expected);
|
||||
// act
|
||||
const actual = sut.getAllScriptsRecursively();
|
||||
// assert
|
||||
expect(actual).to.have.deep.members(expected);
|
||||
});
|
||||
it('gets child categories', () => {
|
||||
// arrange
|
||||
const expectedScriptIds = ['1', '2', '3', '4'];
|
||||
const categories = [
|
||||
new CategoryStub(31).withScriptIds('1', '2'),
|
||||
new CategoryStub(32).withScriptIds('3', '4'),
|
||||
];
|
||||
const sut = new Category(0, 'category', [], categories, []);
|
||||
// act
|
||||
const actualIds = sut.getAllScriptsRecursively().map((s) => s.id);
|
||||
// assert
|
||||
expect(actualIds).to.have.deep.members(expectedScriptIds);
|
||||
|
||||
});
|
||||
it('gets child scripts and categories', () => {
|
||||
// arrange
|
||||
const expectedScriptIds = ['1', '2', '3', '4', '5' , '6'];
|
||||
const categories = [
|
||||
new CategoryStub(31).withScriptIds('1', '2'),
|
||||
new CategoryStub(32).withScriptIds('3', '4'),
|
||||
];
|
||||
const scripts = [ new ScriptStub('5'), new ScriptStub('6') ];
|
||||
const sut = new Category(0, 'category', [], categories, scripts);
|
||||
// act
|
||||
const actualIds = sut.getAllScriptsRecursively().map((s) => s.id);
|
||||
// assert
|
||||
expect(actualIds).to.have.deep.members(expectedScriptIds);
|
||||
|
||||
});
|
||||
it('gets child categories recursively', () => {
|
||||
// arrange
|
||||
const expectedScriptIds = ['1', '2', '3', '4', '5', '6'];
|
||||
const categories = [
|
||||
new CategoryStub(31)
|
||||
.withScriptIds('1', '2')
|
||||
.withCategory(
|
||||
new CategoryStub(32)
|
||||
.withScriptIds('3', '4'),
|
||||
),
|
||||
new CategoryStub(33)
|
||||
.withCategories(
|
||||
new CategoryStub(34)
|
||||
.withScriptIds('5')
|
||||
.withCategory(
|
||||
new CategoryStub(35)
|
||||
.withCategory(
|
||||
new CategoryStub(35).withScriptIds('6'),
|
||||
),
|
||||
),
|
||||
),
|
||||
];
|
||||
// assert
|
||||
const sut = new Category(0, 'category', [], categories, []);
|
||||
// act
|
||||
const actualIds = sut.getAllScriptsRecursively().map((s) => s.id);
|
||||
// assert
|
||||
expect(actualIds).to.have.deep.members(expectedScriptIds);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -95,4 +95,26 @@ describe('InMemoryRepository', () => {
|
||||
expect(actual).to.deep.equal(expected);
|
||||
});
|
||||
});
|
||||
describe('getById', () => {
|
||||
it('gets entity if it exists', () => {
|
||||
// arrange
|
||||
const expected = new NumericEntityStub(1).withCustomProperty('bca');
|
||||
const sut = new InMemoryRepository<number, NumericEntityStub>([
|
||||
expected, new NumericEntityStub(2).withCustomProperty('bca'),
|
||||
new NumericEntityStub(3).withCustomProperty('bca'), new NumericEntityStub(4).withCustomProperty('bca'),
|
||||
]);
|
||||
// act
|
||||
const actual = sut.getById(expected.id);
|
||||
// assert
|
||||
expect(actual).to.deep.equal(expected);
|
||||
});
|
||||
it('gets undefined if it does not exist', () => {
|
||||
// arrange
|
||||
const sut = new InMemoryRepository<number, NumericEntityStub>([]);
|
||||
// act
|
||||
const actual = sut.getById(31);
|
||||
// assert
|
||||
expect(actual).to.equal(undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import { getScriptNodeId, getScriptId, getCategoryNodeId, getCategoryId } from '@/presentation/Scripts/ScriptsTree/ScriptNodeParser';
|
||||
import { CategoryStub } from '../../../stubs/CategoryStub';
|
||||
import { ScriptStub } from '../../../stubs/ScriptStub';
|
||||
import { parseSingleCategory, parseAllCategories } from '../../../../../src/presentation/Scripts/ScriptsTree/ScriptNodeParser';
|
||||
import { ApplicationStub } from '../../../stubs/ApplicationStub';
|
||||
import { INode, NodeType } from '../../../../../src/presentation/Scripts/ScriptsTree/SelectableTree/Node/INode';
|
||||
import { IScript } from '../../../../../src/domain/IScript';
|
||||
import { ICategory } from '../../../../../src/domain/ICategory';
|
||||
|
||||
describe('ScriptNodeParser', () => {
|
||||
it('can convert script id and back', () => {
|
||||
// arrange
|
||||
const script = new ScriptStub('test');
|
||||
// act
|
||||
const nodeId = getScriptNodeId(script);
|
||||
const scriptId = getScriptId(nodeId);
|
||||
// assert
|
||||
expect(scriptId).to.equal(script.id);
|
||||
});
|
||||
it('can convert category id and back', () => {
|
||||
// arrange
|
||||
const category = new CategoryStub(55);
|
||||
// act
|
||||
const nodeId = getCategoryNodeId(category);
|
||||
const scriptId = getCategoryId(nodeId);
|
||||
// assert
|
||||
expect(scriptId).to.equal(category.id);
|
||||
});
|
||||
describe('parseSingleCategory', () => {
|
||||
it('can parse when category has sub categories', () => {
|
||||
// arrange
|
||||
const categoryId = 31;
|
||||
const firstSubCategory = new CategoryStub(11).withScriptIds('111', '112');
|
||||
const secondSubCategory = new CategoryStub(categoryId)
|
||||
.withCategory(new CategoryStub(33).withScriptIds('331', '331'))
|
||||
.withCategory(new CategoryStub(44).withScriptIds('44'));
|
||||
const app = new ApplicationStub().withAction(new CategoryStub(categoryId)
|
||||
.withCategory(firstSubCategory)
|
||||
.withCategory(secondSubCategory));
|
||||
// act
|
||||
const nodes = parseSingleCategory(categoryId, app);
|
||||
// assert
|
||||
expect(nodes).to.have.lengthOf(2);
|
||||
expectSameCategory(nodes[0], firstSubCategory);
|
||||
expectSameCategory(nodes[1], secondSubCategory);
|
||||
});
|
||||
it('can parse when category has sub scripts', () => {
|
||||
// arrange
|
||||
const categoryId = 31;
|
||||
const scripts = [ new ScriptStub('script1'), new ScriptStub('script2'), new ScriptStub('script3') ];
|
||||
const app = new ApplicationStub().withAction(new CategoryStub(categoryId).withScripts(...scripts));
|
||||
// act
|
||||
const nodes = parseSingleCategory(categoryId, app);
|
||||
// assert
|
||||
expect(nodes).to.have.lengthOf(3);
|
||||
expectSameScript(nodes[0], scripts[0]);
|
||||
expectSameScript(nodes[1], scripts[1]);
|
||||
expectSameScript(nodes[2], scripts[2]);
|
||||
});
|
||||
});
|
||||
|
||||
it('parseAllCategories parses as expected', () => {
|
||||
// arrange
|
||||
const app = new ApplicationStub()
|
||||
.withAction(new CategoryStub(0).withScriptIds('1, 2'))
|
||||
.withAction(new CategoryStub(1).withCategories(
|
||||
new CategoryStub(3).withScriptIds('3', '4'),
|
||||
new CategoryStub(4).withCategory(new CategoryStub(5).withScriptIds('6')),
|
||||
));
|
||||
// act
|
||||
const nodes = parseAllCategories(app);
|
||||
// assert
|
||||
expect(nodes).to.have.lengthOf(2);
|
||||
expectSameCategory(nodes[0], app.actions[0]);
|
||||
expectSameCategory(nodes[1], app.actions[1]);
|
||||
});
|
||||
});
|
||||
|
||||
function isReversible(category: ICategory): boolean {
|
||||
if (category.scripts) {
|
||||
return category.scripts.every((s) => s.revertCode);
|
||||
}
|
||||
return category.subCategories.every((c) => isReversible(c));
|
||||
}
|
||||
|
||||
function expectSameCategory(node: INode, category: ICategory): void {
|
||||
expect(node.type).to.equal(NodeType.Category, getErrorMessage('type'));
|
||||
expect(node.id).to.equal(getCategoryNodeId(category), getErrorMessage('id'));
|
||||
expect(node.documentationUrls).to.equal(category.documentationUrls, getErrorMessage('documentationUrls'));
|
||||
expect(node.text).to.equal(category.name, getErrorMessage('name'));
|
||||
expect(node.isReversible).to.equal(isReversible(category), getErrorMessage('isReversible'));
|
||||
expect(node.children).to.have.lengthOf(category.scripts.length || category.subCategories.length, getErrorMessage('name'));
|
||||
for (let i = 0; i < category.subCategories.length; i++) {
|
||||
expectSameCategory(node.children[i], category.subCategories[i]);
|
||||
}
|
||||
for (let i = 0; i < category.scripts.length; i++) {
|
||||
expectSameScript(node.children[i], category.scripts[i]);
|
||||
}
|
||||
function getErrorMessage(field: string) {
|
||||
return `Unexpected node field: ${field}.\n` +
|
||||
`\nActual node:\n${JSON.stringify(node, null, 2)}` +
|
||||
`\nExpected category:\n${JSON.stringify(category, null, 2)}`;
|
||||
}
|
||||
}
|
||||
|
||||
function expectSameScript(node: INode, script: IScript): void {
|
||||
expect(node.type).to.equal(NodeType.Script, getErrorMessage('type'));
|
||||
expect(node.id).to.equal(getScriptNodeId(script), getErrorMessage('id'));
|
||||
expect(node.documentationUrls).to.equal(script.documentationUrls, getErrorMessage('documentationUrls'));
|
||||
expect(node.text).to.equal(script.name, getErrorMessage('name'));
|
||||
expect(node.isReversible).to.equal(!!script.revertCode, getErrorMessage('revertCode'));
|
||||
expect(node.children).to.equal(undefined);
|
||||
function getErrorMessage(field: string) {
|
||||
return `Unexpected node field: ${field}.` +
|
||||
`\nActual node:\n${JSON.stringify(node, null, 2)}\n` +
|
||||
`\nExpected script:\n${JSON.stringify(script, null, 2)}`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import { NodeType, INode } from '@/presentation/Scripts/ScriptsTree/SelectableTree/Node/INode';
|
||||
import { NodePredicateFilter } from '@/presentation/Scripts/ScriptsTree/SelectableTree/LiquorTree/NodeWrapper/NodePredicateFilter';
|
||||
import { ILiquorTreeExistingNode } from 'liquor-tree';
|
||||
|
||||
describe('NodePredicateFilter', () => {
|
||||
it('calls predicate with expected node', () => {
|
||||
// arrange
|
||||
const object: ILiquorTreeExistingNode = {
|
||||
id: 'script',
|
||||
data: {
|
||||
text: 'script-text',
|
||||
type: NodeType.Script,
|
||||
documentationUrls: [],
|
||||
isReversible: false,
|
||||
},
|
||||
states: undefined,
|
||||
children: [],
|
||||
};
|
||||
const expected: INode = {
|
||||
id: 'script',
|
||||
text: 'script-text',
|
||||
isReversible: false,
|
||||
documentationUrls: [],
|
||||
children: [],
|
||||
type: NodeType.Script,
|
||||
};
|
||||
let actual: INode;
|
||||
const predicate = (node: INode) => { actual = node; return true; };
|
||||
const sut = new NodePredicateFilter(predicate);
|
||||
// act
|
||||
sut.matcher('nop query', object);
|
||||
// assert
|
||||
expect(actual).to.deep.equal(expected);
|
||||
});
|
||||
describe('returns result from the predicate', () => {
|
||||
for (const expected of [false, true]) {
|
||||
it(expected.toString(), () => {
|
||||
// arrange
|
||||
const sut = new NodePredicateFilter(() => expected);
|
||||
// act
|
||||
const actual = sut.matcher('nop query', getExistingNode());
|
||||
// assert
|
||||
expect(actual).to.equal(expected);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function getExistingNode(): ILiquorTreeExistingNode {
|
||||
return {
|
||||
id: 'script',
|
||||
data: {
|
||||
text: 'script-text',
|
||||
type: NodeType.Script,
|
||||
documentationUrls: [],
|
||||
isReversible: false,
|
||||
},
|
||||
states: undefined,
|
||||
children: [],
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import { ILiquorTreeNode } from 'liquor-tree';
|
||||
import { NodeType } from '@/presentation/Scripts/ScriptsTree/SelectableTree/Node/INode';
|
||||
import { getNewCheckedState } from '@/presentation/Scripts/ScriptsTree/SelectableTree/LiquorTree/NodeWrapper/NodeStateUpdater';
|
||||
|
||||
describe('getNewCheckedState', () => {
|
||||
describe('script node', () => {
|
||||
it('state is true when selected', () => {
|
||||
// arrange
|
||||
const node = getScriptNode();
|
||||
const selectedScriptNodeIds = [ 'a', 'b', node.id, 'c' ];
|
||||
// act
|
||||
const actual = getNewCheckedState(node, selectedScriptNodeIds);
|
||||
// assert
|
||||
expect(actual).to.equal(true);
|
||||
});
|
||||
it('state is false when unselected', () => {
|
||||
// arrange
|
||||
const node = getScriptNode();
|
||||
const selectedScriptNodeIds = [ 'a', 'b', 'c' ];
|
||||
// act
|
||||
const actual = getNewCheckedState(node, selectedScriptNodeIds);
|
||||
// assert
|
||||
expect(actual).to.equal(false);
|
||||
});
|
||||
});
|
||||
describe('category node', () => {
|
||||
it('state is true when every child selected', () => {
|
||||
// arrange
|
||||
const node = {
|
||||
id: '1',
|
||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||
children: [
|
||||
{ id: '2',
|
||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||
children: [ getScriptNode('a'), getScriptNode('b') ],
|
||||
},
|
||||
{ id: '3',
|
||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||
children: [ getScriptNode('c') ],
|
||||
},
|
||||
],
|
||||
};
|
||||
const selectedScriptNodeIds = [ 'a', 'b', 'c' ];
|
||||
// act
|
||||
const actual = getNewCheckedState(node, selectedScriptNodeIds);
|
||||
// assert
|
||||
expect(actual).to.equal(true);
|
||||
});
|
||||
it('state is false when none of the children is selected', () => {
|
||||
// arrange
|
||||
const node = {
|
||||
id: '1',
|
||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||
children: [
|
||||
{ id: '2',
|
||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||
children: [ getScriptNode('a'), getScriptNode('b') ],
|
||||
},
|
||||
{ id: '3',
|
||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||
children: [ getScriptNode('c') ],
|
||||
},
|
||||
],
|
||||
};
|
||||
const selectedScriptNodeIds = [ 'none', 'of', 'them', 'are', 'selected' ];
|
||||
// act
|
||||
const actual = getNewCheckedState(node, selectedScriptNodeIds);
|
||||
// assert
|
||||
expect(actual).to.equal(false);
|
||||
});
|
||||
it('state is false when some of the children is selected', () => {
|
||||
// arrange
|
||||
const node = {
|
||||
id: '1',
|
||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||
children: [
|
||||
{
|
||||
id: '2',
|
||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||
children: [ getScriptNode('a'), getScriptNode('b') ],
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||
children: [ getScriptNode('c') ],
|
||||
},
|
||||
],
|
||||
};
|
||||
const selectedScriptNodeIds = [ 'a', 'c', 'unrelated' ];
|
||||
// act
|
||||
const actual = getNewCheckedState(node, selectedScriptNodeIds);
|
||||
// assert
|
||||
expect(actual).to.equal(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function getScriptNode(scriptNodeId: string = 'script'): ILiquorTreeNode {
|
||||
return {
|
||||
id: scriptNodeId,
|
||||
data: {
|
||||
type: NodeType.Script,
|
||||
documentationUrls: [],
|
||||
isReversible: false,
|
||||
},
|
||||
children: [],
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
import 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import { NodeType, INode } from '@/presentation/Scripts/ScriptsTree/SelectableTree/Node/INode';
|
||||
import { ILiquorTreeExistingNode, ILiquorTreeNewNode, ILiquorTreeNodeData, ICustomLiquorTreeData } from 'liquor-tree';
|
||||
import { convertExistingToNode, toNewLiquorTreeNode } from '@/presentation/Scripts/ScriptsTree/SelectableTree/LiquorTree/NodeWrapper/NodeTranslator';
|
||||
|
||||
describe('NodeTranslator', () => {
|
||||
it('convertExistingToNode', () => {
|
||||
// arrange
|
||||
const existingNode = getExistingNode();
|
||||
const expected = getNode();
|
||||
// act
|
||||
const actual = convertExistingToNode(existingNode);
|
||||
// assert
|
||||
expect(actual).to.deep.equal(expected);
|
||||
});
|
||||
it('toNewLiquorTreeNode', () => {
|
||||
// arrange
|
||||
const node = getNode();
|
||||
const expected = getNewNode();
|
||||
// act
|
||||
const actual = toNewLiquorTreeNode(node);
|
||||
// assert
|
||||
expect(actual).to.deep.equal(expected);
|
||||
});
|
||||
});
|
||||
|
||||
function getNode(): INode {
|
||||
return {
|
||||
id: '1',
|
||||
text: 'parentcategory',
|
||||
isReversible: true,
|
||||
type: NodeType.Category,
|
||||
documentationUrls: [ 'parentcategory-url1', 'parentcategory-url2 '],
|
||||
children: [
|
||||
{
|
||||
id: '2',
|
||||
text: 'subcategory',
|
||||
isReversible: true,
|
||||
documentationUrls: [ 'subcategory-url1', 'subcategory-url2 '],
|
||||
type: NodeType.Category,
|
||||
children: [
|
||||
{
|
||||
id: 'script1',
|
||||
text: 'cool script 1',
|
||||
isReversible: true,
|
||||
documentationUrls: [ 'script1url1', 'script1url2'],
|
||||
children: [],
|
||||
type: NodeType.Script,
|
||||
},
|
||||
{
|
||||
id: 'script2',
|
||||
text: 'cool script 2',
|
||||
isReversible: true,
|
||||
documentationUrls: [ 'script2url1', 'script2url2'],
|
||||
children: [],
|
||||
type: NodeType.Script,
|
||||
}],
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
||||
function getExpectedExistingNodeData(node: INode): ILiquorTreeNodeData {
|
||||
return {
|
||||
text: node.text,
|
||||
type: node.type,
|
||||
documentationUrls: node.documentationUrls,
|
||||
isReversible: node.isReversible,
|
||||
};
|
||||
}
|
||||
function getExpectedNewNodeData(node: INode): ICustomLiquorTreeData {
|
||||
return {
|
||||
type: node.type,
|
||||
documentationUrls: node.documentationUrls,
|
||||
isReversible: node.isReversible,
|
||||
};
|
||||
}
|
||||
|
||||
function getExistingNode(): ILiquorTreeExistingNode {
|
||||
const base = getNode();
|
||||
return {
|
||||
id: base.id,
|
||||
data: getExpectedExistingNodeData(base),
|
||||
states: undefined,
|
||||
children: [
|
||||
{
|
||||
id: base.children[0].id,
|
||||
data: getExpectedExistingNodeData(base.children[0]),
|
||||
states: undefined,
|
||||
children: [
|
||||
{
|
||||
id: base.children[0].children[0].id,
|
||||
data: getExpectedExistingNodeData(base.children[0].children[0]),
|
||||
states: undefined,
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: base.children[0].children[1].id,
|
||||
data: getExpectedExistingNodeData(base.children[0].children[1]),
|
||||
states: undefined,
|
||||
children: [],
|
||||
}],
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
||||
function getNewNode(): ILiquorTreeNewNode {
|
||||
const base = getNode();
|
||||
const commonState = {
|
||||
checked: false,
|
||||
};
|
||||
return {
|
||||
id: base.id,
|
||||
text: base.text,
|
||||
data: getExpectedNewNodeData(base),
|
||||
state: commonState,
|
||||
children: [
|
||||
{
|
||||
id: base.children[0].id,
|
||||
text: base.children[0].text,
|
||||
data: getExpectedNewNodeData(base.children[0]),
|
||||
state: commonState,
|
||||
children: [
|
||||
{
|
||||
id: base.children[0].children[0].id,
|
||||
text: base.children[0].children[0].text,
|
||||
data: getExpectedNewNodeData(base.children[0].children[0]),
|
||||
state: commonState,
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: base.children[0].children[1].id,
|
||||
text: base.children[0].children[1].text,
|
||||
data: getExpectedNewNodeData(base.children[0].children[1]),
|
||||
state: commonState,
|
||||
children: [],
|
||||
}],
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import { ScriptStub } from '../../../../../../stubs/ScriptStub';
|
||||
import { CategoryReverter } from '@/presentation/Scripts/ScriptsTree/SelectableTree/Node/Reverter/CategoryReverter';
|
||||
import { getCategoryNodeId } from '@/presentation/Scripts/ScriptsTree/ScriptNodeParser';
|
||||
import { CategoryStub } from '../../../../../../stubs/CategoryStub';
|
||||
import { ApplicationStub } from '../../../../../../stubs/ApplicationStub';
|
||||
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
||||
import { UserSelection } from '@/application/State/Selection/UserSelection';
|
||||
|
||||
describe('CategoryReverter', () => {
|
||||
describe('getState', () => {
|
||||
// arrange
|
||||
const scripts = [
|
||||
new ScriptStub('revertable').withRevertCode('REM revert me'),
|
||||
new ScriptStub('revertable2').withRevertCode('REM revert me 2'),
|
||||
];
|
||||
const category = new CategoryStub(1).withScripts(...scripts);
|
||||
const nodeId = getCategoryNodeId(category);
|
||||
const app = new ApplicationStub().withAction(category);
|
||||
const sut = new CategoryReverter(nodeId, app);
|
||||
const testCases = [
|
||||
{
|
||||
name: 'false when subscripts are not reverted',
|
||||
state: scripts.map((script) => new SelectedScript(script, false)),
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: 'false when some subscripts are reverted',
|
||||
state: [new SelectedScript(scripts[0], false), new SelectedScript(scripts[0], true)],
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: 'false when subscripts are not reverted',
|
||||
state: scripts.map((script) => new SelectedScript(script, true)),
|
||||
expected: true,
|
||||
},
|
||||
];
|
||||
for (const testCase of testCases) {
|
||||
it(testCase.name, () => {
|
||||
// act
|
||||
const actual = sut.getState(testCase.state);
|
||||
// assert
|
||||
expect(actual).to.equal(testCase.expected);
|
||||
});
|
||||
}
|
||||
});
|
||||
describe('selectWithRevertState', () => {
|
||||
// arrange
|
||||
const scripts = [
|
||||
new ScriptStub('revertable').withRevertCode('REM revert me'),
|
||||
new ScriptStub('revertable2').withRevertCode('REM revert me 2'),
|
||||
];
|
||||
const category = new CategoryStub(1).withScripts(...scripts);
|
||||
const app = new ApplicationStub().withAction(category);
|
||||
const testCases = [
|
||||
{
|
||||
name: 'selects with revert state when not selected',
|
||||
selection: [],
|
||||
revert: true, expectRevert: true,
|
||||
},
|
||||
{
|
||||
name: 'selects with non-revert state when not selected',
|
||||
selection: [],
|
||||
revert: false, expectRevert: false,
|
||||
},
|
||||
{
|
||||
name: 'switches when already selected with revert state',
|
||||
selection: scripts.map((script) => new SelectedScript(script, true)),
|
||||
revert: false, expectRevert: false,
|
||||
},
|
||||
{
|
||||
name: 'switches when already selected with not revert state',
|
||||
selection: scripts.map((script) => new SelectedScript(script, false)),
|
||||
revert: true, expectRevert: true,
|
||||
},
|
||||
{
|
||||
name: 'keeps revert state when already selected with revert state',
|
||||
selection: scripts.map((script) => new SelectedScript(script, true)),
|
||||
revert: true, expectRevert: true,
|
||||
},
|
||||
{
|
||||
name: 'keeps revert state deselected when already selected wtih non revert state',
|
||||
selection: scripts.map((script) => new SelectedScript(script, false)),
|
||||
revert: false, expectRevert: false,
|
||||
},
|
||||
];
|
||||
const nodeId = getCategoryNodeId(category);
|
||||
for (const testCase of testCases) {
|
||||
it(testCase.name, () => {
|
||||
const selection = new UserSelection(app, testCase.selection);
|
||||
const sut = new CategoryReverter(nodeId, app);
|
||||
// act
|
||||
sut.selectWithRevertState(testCase.revert, selection);
|
||||
// assert
|
||||
expect(sut.getState(selection.selectedScripts)).to.equal(testCase.expectRevert);
|
||||
expect(selection.selectedScripts).has.lengthOf(2);
|
||||
expect(selection.selectedScripts[0].id).equal(scripts[0].id);
|
||||
expect(selection.selectedScripts[1].id).equal(scripts[1].id);
|
||||
expect(selection.selectedScripts[0].revert).equal(testCase.expectRevert);
|
||||
expect(selection.selectedScripts[1].revert).equal(testCase.expectRevert);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
import 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import { INode, NodeType } from '@/presentation/Scripts/ScriptsTree/SelectableTree/Node/INode';
|
||||
import { getReverter } from '@/presentation/Scripts/ScriptsTree/SelectableTree/Node/Reverter/ReverterFactory';
|
||||
import { ScriptReverter } from '@/presentation/Scripts/ScriptsTree/SelectableTree/Node/Reverter/ScriptReverter';
|
||||
import { CategoryReverter } from '@/presentation/Scripts/ScriptsTree/SelectableTree/Node/Reverter/CategoryReverter';
|
||||
import { ApplicationStub } from '../../../../../../stubs/ApplicationStub';
|
||||
import { CategoryStub } from '../../../../../../stubs/CategoryStub';
|
||||
import { getScriptNodeId, getCategoryNodeId } from '@/presentation/Scripts/ScriptsTree/ScriptNodeParser';
|
||||
import { ScriptStub } from '../../../../../../stubs/ScriptStub';
|
||||
|
||||
describe('ReverterFactory', () => {
|
||||
describe('getReverter', () => {
|
||||
it('gets CategoryReverter for category node', () => {
|
||||
// arrange
|
||||
const category = new CategoryStub(0).withScriptIds('55');
|
||||
const node = getNodeStub(getCategoryNodeId(category), NodeType.Category);
|
||||
const app = new ApplicationStub().withAction(category);
|
||||
// act
|
||||
const result = getReverter(node, app);
|
||||
// assert
|
||||
expect(result instanceof CategoryReverter).to.equal(true);
|
||||
});
|
||||
it('gets ScriptReverter for script node', () => {
|
||||
// arrange
|
||||
const script = new ScriptStub('test');
|
||||
const node = getNodeStub(getScriptNodeId(script), NodeType.Script);
|
||||
const app = new ApplicationStub().withAction(new CategoryStub(0).withScript(script));
|
||||
// act
|
||||
const result = getReverter(node, app);
|
||||
// assert
|
||||
expect(result instanceof ScriptReverter).to.equal(true);
|
||||
});
|
||||
});
|
||||
function getNodeStub(nodeId: string, type: NodeType): INode {
|
||||
return {
|
||||
id: nodeId,
|
||||
text: 'text',
|
||||
isReversible: false,
|
||||
documentationUrls: [],
|
||||
children: [],
|
||||
type,
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,88 @@
|
||||
import 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import { ScriptReverter } from '@/presentation/Scripts/ScriptsTree/SelectableTree/Node/Reverter/ScriptReverter';
|
||||
import { SelectedScriptStub } from '../../../../../../stubs/SelectedScriptStub';
|
||||
import { getScriptNodeId } from '@/presentation/Scripts/ScriptsTree/ScriptNodeParser';
|
||||
import { ScriptStub } from '../../../../../../stubs/ScriptStub';
|
||||
import { UserSelection } from '../../../../../../../../src/application/State/Selection/UserSelection';
|
||||
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
||||
import { ApplicationStub } from '../../../../../../stubs/ApplicationStub';
|
||||
import { CategoryStub } from '../../../../../../stubs/CategoryStub';
|
||||
|
||||
describe('ScriptReverter', () => {
|
||||
describe('getState', () => {
|
||||
it('false when script is not selected', () => {
|
||||
// arrange
|
||||
const script = new ScriptStub('id');
|
||||
const nodeId = getScriptNodeId(script);
|
||||
const sut = new ScriptReverter(nodeId);
|
||||
// act
|
||||
const actual = sut.getState([]);
|
||||
// assert
|
||||
expect(actual).to.equal(false);
|
||||
});
|
||||
it('false when script is selected but not reverted', () => {
|
||||
// arrange
|
||||
const scripts = [ new SelectedScriptStub('id'), new SelectedScriptStub('dummy') ];
|
||||
const nodeId = getScriptNodeId(scripts[0].script);
|
||||
const sut = new ScriptReverter(nodeId);
|
||||
// act
|
||||
const actual = sut.getState(scripts);
|
||||
// assert
|
||||
expect(actual).to.equal(false);
|
||||
});
|
||||
it('true when script is selected and reverted', () => {
|
||||
// arrange
|
||||
const scripts = [ new SelectedScriptStub('id', true), new SelectedScriptStub('dummy') ];
|
||||
const nodeId = getScriptNodeId(scripts[0].script);
|
||||
const sut = new ScriptReverter(nodeId);
|
||||
// act
|
||||
const actual = sut.getState(scripts);
|
||||
// assert
|
||||
expect(actual).to.equal(true);
|
||||
});
|
||||
});
|
||||
describe('selectWithRevertState', () => {
|
||||
// arrange
|
||||
const script = new ScriptStub('id');
|
||||
const app = new ApplicationStub().withAction(new CategoryStub(5).withScript(script));
|
||||
const testCases = [
|
||||
{
|
||||
name: 'selects with revert state when not selected',
|
||||
selection: [], revert: true, expectRevert: true,
|
||||
},
|
||||
{
|
||||
name: 'selects with non-revert state when not selected',
|
||||
selection: [], revert: false, expectRevert: false,
|
||||
},
|
||||
{
|
||||
name: 'switches when already selected with revert state',
|
||||
selection: [ new SelectedScript(script, true)], revert: false, expectRevert: false,
|
||||
},
|
||||
{
|
||||
name: 'switches when already selected with not revert state',
|
||||
selection: [ new SelectedScript(script, false)], revert: true, expectRevert: true,
|
||||
},
|
||||
{
|
||||
name: 'keeps revert state when already selected with revert state',
|
||||
selection: [ new SelectedScript(script, true)], revert: true, expectRevert: true,
|
||||
},
|
||||
{
|
||||
name: 'keeps revert state deselected when already selected wtih non revert state',
|
||||
selection: [ new SelectedScript(script, false)], revert: false, expectRevert: false,
|
||||
},
|
||||
];
|
||||
const nodeId = getScriptNodeId(script);
|
||||
for (const testCase of testCases) {
|
||||
it(testCase.name, () => {
|
||||
const selection = new UserSelection(app, testCase.selection);
|
||||
const sut = new ScriptReverter(nodeId);
|
||||
// act
|
||||
sut.selectWithRevertState(testCase.revert, selection);
|
||||
// assert
|
||||
expect(selection.isSelected(script.id)).to.equal(true);
|
||||
expect(selection.selectedScripts[0].revert).equal(testCase.expectRevert);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -8,15 +8,16 @@ export class ApplicationStub implements IApplication {
|
||||
public readonly version = '0.1.0';
|
||||
public readonly actions = new Array<ICategory>();
|
||||
|
||||
public withAction(category: ICategory): IApplication {
|
||||
public withAction(category: ICategory): ApplicationStub {
|
||||
this.actions.push(category);
|
||||
return this;
|
||||
}
|
||||
public findCategory(categoryId: number): ICategory {
|
||||
throw new Error('Method not implemented.');
|
||||
return this.getAllCategories().find(
|
||||
(category) => category.id === categoryId);
|
||||
}
|
||||
public getRecommendedScripts(): readonly IScript[] {
|
||||
throw new Error('Method not implemented.');
|
||||
throw new Error('Method not implemented: getRecommendedScripts');
|
||||
}
|
||||
public findScript(scriptId: string): IScript {
|
||||
return this.getAllScripts().find((script) => scriptId === script.id);
|
||||
|
||||
@@ -11,6 +11,14 @@ export class CategoryStub extends BaseEntity<number> implements ICategory {
|
||||
constructor(id: number) {
|
||||
super(id);
|
||||
}
|
||||
|
||||
public getAllScriptsRecursively(): readonly IScript[] {
|
||||
return [
|
||||
...this.scripts,
|
||||
...this.subCategories.flatMap((c) => c.getAllScriptsRecursively()),
|
||||
];
|
||||
}
|
||||
|
||||
public withScriptIds(...scriptIds: string[]): CategoryStub {
|
||||
for (const scriptId of scriptIds) {
|
||||
this.withScript(new ScriptStub(scriptId));
|
||||
|
||||
8
tests/unit/stubs/SelectedScriptStub.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
||||
import { ScriptStub } from './ScriptStub';
|
||||
|
||||
export class SelectedScriptStub extends SelectedScript {
|
||||
constructor(id: string, revert = false) {
|
||||
super(new ScriptStub(id), revert);
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,6 @@ module.exports = {
|
||||
electronBuilder: {
|
||||
// https://www.electron.build/configuration/configuration
|
||||
builderOptions: {
|
||||
win: {
|
||||
icon: './public/favicon.ico'
|
||||
},
|
||||
publish: [{
|
||||
// https://www.electron.build/configuration/publish#githuboptions
|
||||
// https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/recipes.html#enable-publishing-to-github
|
||||
|
||||