Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92a7118d1c | ||
|
|
a9f9e90443 | ||
|
|
31d2067f07 | ||
|
|
dd7e1416b4 | ||
|
|
1d5225de07 | ||
|
|
9c063d59de | ||
|
|
57028987f1 | ||
|
|
9e722ddfb3 | ||
|
|
646a8e0b9f | ||
|
|
f27a2871d7 | ||
|
|
909c44d72a | ||
|
|
53cf595e17 | ||
|
|
2c4eb78c3f | ||
|
|
d7a1325c0b | ||
|
|
30efbcc621 | ||
|
|
628c16eb95 | ||
|
|
d8552c62ff | ||
|
|
df84083536 | ||
|
|
461a4f122b | ||
|
|
c937af8ee7 | ||
|
|
636d4279c8 | ||
|
|
019b838925 | ||
|
|
0fc18459cd | ||
|
|
583c5660d6 | ||
|
|
52d5713a99 | ||
|
|
b34a66f270 | ||
|
|
eed996f608 | ||
|
|
b96c5d0557 | ||
|
|
aab8f21a8d | ||
|
|
c668a97950 | ||
|
|
bb98d20637 | ||
|
|
e2ab124fb7 | ||
|
|
0d2efe5b05 | ||
|
|
156a6554ef |
9
.github/workflows/bump-and-release.yaml
vendored
9
.github/workflows/bump-and-release.yaml
vendored
@@ -1,19 +1,13 @@
|
||||
name: Bump & release
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [closed]
|
||||
branches:
|
||||
- master
|
||||
push: # Ensure a new release is created for each new tag
|
||||
tags:
|
||||
- '[0-9]+.[0-9]+.[0-9]+'
|
||||
|
||||
jobs:
|
||||
bump-version-and-release:
|
||||
if: > # Push => Ensure only changes from master. PR => to not trigger when closing PR without merging
|
||||
(github.event_name == 'push' && github.event.base_ref == 'refs/heads/master')
|
||||
|| github.event.pull_request.merged == true
|
||||
if: github.event.base_ref == 'refs/heads/master'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
@@ -21,3 +15,4 @@ jobs:
|
||||
with:
|
||||
user: undergroundwires-bot
|
||||
release-token: ${{secrets.BUMP_GITHUB_PAT}} # Does not trigger release pipeline if we use default token: https://github.community/t5/GitHub-Actions/Github-Action-trigger-on-release-not-working-if-releases-was/td-p/34559
|
||||
# GitHub does not inject secrets if pipeline runs from fork or a fork is merged to main repo.
|
||||
139
CHANGELOG.md
Normal file
139
CHANGELOG.md
Normal file
@@ -0,0 +1,139 @@
|
||||
# Changelog
|
||||
|
||||
## 0.4.10 (2020-07-15)
|
||||
|
||||
* fixed script validation errors | [commit](https://github.com/undergroundwires/privacy.sexy/commit/a34ae139d7fc1ba05b8ab9eb962da4ca0231ed5c)
|
||||
|
||||
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.4.9...0.4.10)
|
||||
|
||||
## 0.4.9 (2020-07-14)
|
||||
|
||||
* disable office telemetry Disassembler0/Win10-Initial-Setup-Script#288 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/53cf595e1726ee3de79137fd566978fd512d218f)
|
||||
* updated to may 2020 update | [commit](https://github.com/undergroundwires/privacy.sexy/commit/909c44d72a4a602ee8f27d06b6ec706c1e432ce1)
|
||||
* simplified docker builds | [commit](https://github.com/undergroundwires/privacy.sexy/commit/f27a2871d74e5117fc029be82caef12246e10879)
|
||||
|
||||
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.4.8...0.4.9)
|
||||
|
||||
## 0.4.8 (2020-07-11)
|
||||
|
||||
* added more scripts #16 (#17) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/d8552c62ffea13ce62abce836c7dd4980eef6bb9)
|
||||
* stopping services before disabling #16 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/628c16eb952495f5b3f6d794161b355f4b08b819)
|
||||
* can disable features, capabilities & remove onedrive #16 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/30efbcc621eb83dd5a9c1e66b8f1f5350eb95006)
|
||||
* updated one more typo (#19) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/d7a1325c0b7665ce712dc411965d00fc1d6fa384)
|
||||
* more tweaks #16 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/2c4eb78c3f156cb0d033977cffbe7464697680f5)
|
||||
|
||||
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.4.7...0.4.8)
|
||||
|
||||
## 0.4.7 (2020-06-30)
|
||||
|
||||
* removed HKU tweak as all HKU's are changed #10 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/c937af8ee7da9aa95131e56abf7bf24800390fe6)
|
||||
* Fixed types + script in "Clear Windows log files" (#15) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/461a4f122b342369db5cc08c5e30961c64e68cdd)
|
||||
|
||||
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.4.6...0.4.7)
|
||||
|
||||
## 0.4.6 (2020-06-16)
|
||||
|
||||
* Fixed Some More Issues (#12) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/52d5713a99422cdf900aba819e49e998abac33cc)
|
||||
* removed failing continuous deployment #14 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/583c5660d6ac934b845a044e013357aa91f61c15)
|
||||
* Updated Some Tweaks (#11) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/0fc18459cde57684f00764815062f838f932aed5)
|
||||
* Updated Some More Tweaks (#13) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/019b838925e963b7ec052ac76c6faf5650b9eb67)
|
||||
|
||||
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.4.5...0.4.6)
|
||||
|
||||
## 0.4.5 (2020-06-13)
|
||||
|
||||
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.4.4...0.4.5)
|
||||
|
||||
## 0.4.4 (2020-05-24)
|
||||
|
||||
* fixed close card button not being visible & cleanup | [commit](https://github.com/undergroundwires/privacy.sexy/commit/0d2efe5b05aa965458b78b8fa43754ce2f4fe11b)
|
||||
* new footer with privacy policy | [commit](https://github.com/undergroundwires/privacy.sexy/commit/e2ab124fb799f56ada3570fdc911361cb803e889)
|
||||
* one command to lint everything "npm run lint" | [commit](https://github.com/undergroundwires/privacy.sexy/commit/bb98d20637cbf1d524ebb2973e308773006e3153)
|
||||
* fix "group by" overflows on smaller screens | [commit](https://github.com/undergroundwires/privacy.sexy/commit/c668a97950a1cb7c8bf2a7fd8a72d1101e65e8ce)
|
||||
* clicking outside of a card closes it | [commit](https://github.com/undergroundwires/privacy.sexy/commit/aab8f21a8d8dbed54798af581e6e1ad9e86a4be1)
|
||||
|
||||
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.4.3...0.4.4)
|
||||
|
||||
## 0.4.3 (2020-05-23)
|
||||
|
||||
* removed redundant documentation | [commit](https://github.com/undergroundwires/privacy.sexy/commit/749a140eb8dba09cb67fec2f8dec937e66e3cff5)
|
||||
* fixed broke link | [commit](https://github.com/undergroundwires/privacy.sexy/commit/97b7e03233d9718a8df30cb01ce06ca9489a0295)
|
||||
* simplified heading | [commit](https://github.com/undergroundwires/privacy.sexy/commit/226074c5342f7463c06fcff1457d352ca30295a3)
|
||||
* reading version from package.json instead of version file #5 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/691f989682179016ddcbf55a05cded29155288c9)
|
||||
* automatically increases patch number #5 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/3e3bc07576f7c7e74e3e11fc7d197cbb9a9fb8c0)
|
||||
* using deployment operations from aws-static-site-with-cd | [commit](https://github.com/undergroundwires/privacy.sexy/commit/997be7113f676888892ffa35566d9ebb58a3e9ea)
|
||||
* automated using bump-everywhere + more quality checks (#8) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/4a91e8ccd8a707bc6bea34ee28cff7fa4f66ee2f)
|
||||
|
||||
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.4.2...0.4.3)
|
||||
|
||||
## 0.4.2 (2020-02-29)
|
||||
|
||||
* added missing semicolon for masking | [commit](https://github.com/undergroundwires/privacy.sexy/commit/e63ac4ae67da68243a525af149ff30e5d485b641)
|
||||
* set font on input | [commit](https://github.com/undergroundwires/privacy.sexy/commit/0c39a06be5e4b0a2031ad5e9f5220dd669afee53)
|
||||
* shortened all HKEY paths | [commit](https://github.com/undergroundwires/privacy.sexy/commit/802b36bdd8dcc1f0a2853fe7da2ea2fccd69a88c)
|
||||
|
||||
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.4.1...0.4.2)
|
||||
|
||||
## 0.4.1 (2020-01-11)
|
||||
|
||||
* fixed search bug | [commit](https://github.com/undergroundwires/privacy.sexy/commit/31364bdfec503af09ffbb58044a17dfb833fc8d9)
|
||||
* hide grouping while searching | [commit](https://github.com/undergroundwires/privacy.sexy/commit/92f1a36bcb1e1fe7c90efe8ccd3ede55991e9d9c)
|
||||
* 👀🔍 showing search queries | [commit](https://github.com/undergroundwires/privacy.sexy/commit/97a7747933d2b515cc03ab8243e6a8ae702ef16a)
|
||||
* more efficient queries with single lowercase | [commit](https://github.com/undergroundwires/privacy.sexy/commit/19813b691746d98670823025c460480400e34b6e)
|
||||
* using right 🔍 input type | [commit](https://github.com/undergroundwires/privacy.sexy/commit/0ce354ea0956391ad3f37b252daac1127bfc601a)
|
||||
|
||||
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.4.0...0.4.1)
|
||||
|
||||
## 0.4.0 (2020-01-11)
|
||||
|
||||
* 🔍 support for search | [commit](https://github.com/undergroundwires/privacy.sexy/commit/89862b2775703257b9dc2e19fbebde2c0d0fbda0)
|
||||
* more scripts & better organized | [commit](https://github.com/undergroundwires/privacy.sexy/commit/95baf3175b0d2c7df516f7893a96346b94ac8eca)
|
||||
* refactorings | [commit](https://github.com/undergroundwires/privacy.sexy/commit/e3f82e069e305f6d94eab335470c8e7b44295dd6)
|
||||
* more margin for the scripts | [commit](https://github.com/undergroundwires/privacy.sexy/commit/5ea46ecbf52236953d19f09a8eade08b83e6cd34)
|
||||
|
||||
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.3.0...0.4.0)
|
||||
|
||||
## 0.3.0 (2020-01-09)
|
||||
|
||||
* added description & more descriptive title | [commit](https://github.com/undergroundwires/privacy.sexy/commit/99576340b648550149871e2c0fe0b0d8c2dd0d7c)
|
||||
* allow robots | [commit](https://github.com/undergroundwires/privacy.sexy/commit/eee0e785ec2c5e6bed53d21b4126a57773e35dba)
|
||||
* removed unused references | [commit](https://github.com/undergroundwires/privacy.sexy/commit/cfd888f3afc5c260a0a4a73f2843b86b9f1df2cd)
|
||||
* 🚫 disable NVIDIA telemetry | [commit](https://github.com/undergroundwires/privacy.sexy/commit/ab28f4ed8538d51e1777c86302a63a0cd9c3cb2a)
|
||||
* backwards compatibility for fonts | [commit](https://github.com/undergroundwires/privacy.sexy/commit/4bc13e11926a6df77079646499e799742153b4ab)
|
||||
* added back meta needed for responsiveness | [commit](https://github.com/undergroundwires/privacy.sexy/commit/ed872ef3d9f6c92afc0ce0d06998c60463a8b4e8)
|
||||
* fancy-font is renamed to main and now used | [commit](https://github.com/undergroundwires/privacy.sexy/commit/6825001c61426194dc363b96b57a321241f3ba57)
|
||||
* added support for grouping | [commit](https://github.com/undergroundwires/privacy.sexy/commit/ec6b3c54072a77bb4305da1c234db6c649218b88)
|
||||
* less hyphens as it looks better on mobile | [commit](https://github.com/undergroundwires/privacy.sexy/commit/e0b080af69157f46ba12e2c25e794f5384671b51)
|
||||
|
||||
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.2.0...0.3.0)
|
||||
|
||||
## 0.2.0 (2020-01-06)
|
||||
|
||||
* added GitHub Actions badge for build & deploy | [commit](https://github.com/undergroundwires/privacy.sexy/commit/a229aca68a92bbcd8e8176ac1dd25ce03509e074)
|
||||
* more badges 📛🏆📜 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/090e8319091044e53484ba8338510f6fb7c3cb80)
|
||||
* typo fixes + whitespace refactorings | [commit](https://github.com/undergroundwires/privacy.sexy/commit/e99f210c9dcf61a21e445e2a331384b6066f2c98)
|
||||
* switched content information to "why" section | [commit](https://github.com/undergroundwires/privacy.sexy/commit/beb3c8339f83a224ca66ad8a911a9265ffe7c9c0)
|
||||
* fixed contribution URL | [commit](https://github.com/undergroundwires/privacy.sexy/commit/7b4277d7706ccf6ba7e4b7b01aa46f8e3852cfc6)
|
||||
* fixed wrong relation + lighter style | [commit](https://github.com/undergroundwires/privacy.sexy/commit/8d05b03c9f3c9fc015be6615da8c283809712065)
|
||||
* better URL validation | [commit](https://github.com/undergroundwires/privacy.sexy/commit/aff463dd64fecff92a786fcba88621dff6b1cf73)
|
||||
* refactoring to new function | [commit](https://github.com/undergroundwires/privacy.sexy/commit/c646c102730481c3f4648eb714dc0a84ce35b13c)
|
||||
* optimized find queries & refactorings | [commit](https://github.com/undergroundwires/privacy.sexy/commit/d38f6cd6a8b33e11df854c7abea05974dc04d4ce)
|
||||
* 🎨 styled no JS error | [commit](https://github.com/undergroundwires/privacy.sexy/commit/c359f1d89c6874b3cc94154b993e33f58bd32268)
|
||||
* simplified finding duplicates | [commit](https://github.com/undergroundwires/privacy.sexy/commit/57037aaefcc0e80f0f4719cea89568490a731028)
|
||||
* fixed maintainability badge URL | [commit](https://github.com/undergroundwires/privacy.sexy/commit/aaea47e7d15fe41dea26968db0107a0c53d108f3)
|
||||
* fixed wrong line dumps | [commit](https://github.com/undergroundwires/privacy.sexy/commit/5ccc7c59528885ae7729197df3dfa00f924a2b3f)
|
||||
* refactorings in parsing | [commit](https://github.com/undergroundwires/privacy.sexy/commit/2aa3742e30646bf1d1f3779419d161c3fb6c4808)
|
||||
* using free function | [commit](https://github.com/undergroundwires/privacy.sexy/commit/20020af7c1d8de13948d8761fd4e7f0affb2badb)
|
||||
* default selection is now none | [commit](https://github.com/undergroundwires/privacy.sexy/commit/3140cc663b86394d543de90228aa53e6a304d8d9)
|
||||
* added hyphen lines for longer names | [commit](https://github.com/undergroundwires/privacy.sexy/commit/cced601d686d550f4225018e5311b7433efbb5ae)
|
||||
* more descriptive subtitle | [commit](https://github.com/undergroundwires/privacy.sexy/commit/2cf9214b14d9720f747a71b3864ba7a28acf0ff4)
|
||||
* added footer with version | [commit](https://github.com/undergroundwires/privacy.sexy/commit/10a34fae2f1a219ec52db0c74edb39b46ebd8abc)
|
||||
* using font variables | [commit](https://github.com/undergroundwires/privacy.sexy/commit/60e6348dc8d53f1e81ebdb2ec0e1962aac1e9842)
|
||||
* code-gen refactorings | [commit](https://github.com/undergroundwires/privacy.sexy/commit/246e753ddc9dc8bf630e538663584bf3423cc749)
|
||||
* added text when nothing is chosen | [commit](https://github.com/undergroundwires/privacy.sexy/commit/a7da75d4428090423b692ce45423f5bd300d8442)
|
||||
|
||||
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.1.0...0.2.0)
|
||||
|
||||
## 0.1.0 (2019-12-31)
|
||||
|
||||
Initial release | [commits](https://github.com/undergroundwires/privacy.sexy/commit/4e7f244190c6ffbf7b20443e3e69cf2402c4268a)
|
||||
46
CONTRIBUTING.md
Normal file
46
CONTRIBUTING.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Contributing
|
||||
|
||||
- Love your input! Contributing to this project should be as easy and transparent as possible, whether it's:
|
||||
- Reporting a bug
|
||||
- Discussing the current state of the code
|
||||
- Submitting a fix
|
||||
- Proposing new features
|
||||
- Becoming a maintainer
|
||||
|
||||
## Pull Request Process
|
||||
|
||||
- [GitHub flow](https://guides.github.com/introduction/flow/index.html) is used
|
||||
- Your pull requests are actively welcomed.
|
||||
- The steps:
|
||||
1. Fork the repo and create your branch from master.
|
||||
2. If you've added code that should be tested, add tests.
|
||||
3. If you've changed APIs, update the documentation.
|
||||
4. Ensure the test suite passes.
|
||||
5. Make sure your code lints.
|
||||
6. Issue that pull request!
|
||||
- 🙏 DO
|
||||
- Document your changes in the pull request
|
||||
- 💡 Check [developer notes](./docs/developer-notes.md) if you need help
|
||||
- ❗ DON'T
|
||||
- Do not update the versions, current version is only [set by the maintainer](./docs/gitops.png) and updated automatically by [bump-everywhere](https://github.com/undergroundwires/bump-everywhere)
|
||||
|
||||
## Guidelines
|
||||
|
||||
### Extend scripts
|
||||
|
||||
- Create a [pull request](./../CONTRIBUTING.md#Pull+Request+Process) for [application.yaml](./../src/application/application.yaml)
|
||||
- 🙏 For any new script, try to add `revertCode` that'll revert the changes caused by the script.
|
||||
- See [typings](./../src/application/application.yaml.d.ts) for documentation as code.
|
||||
|
||||
### Handle the state in presentation layer
|
||||
|
||||
- There are two types of components:
|
||||
- **Stateless**, extends `Vue`
|
||||
- **Stateful**, extends [`StatefulVue`](./src/presentation/StatefulVue.ts)
|
||||
- The source of truth for the state lies in [application layer](./src/application/state) and must be updated from the views if they're mutating the state
|
||||
- They mutate or/and reacts to changes in [application state](./src/application/state).
|
||||
- You can react by getting the state and listening to it and update the view accordingly in [`mounted()`](https://vuejs.org/v2/api/#mounted) method.
|
||||
|
||||
## License
|
||||
|
||||
By contributing, you agree that your contributions will be licensed under its GNU General Public License v3.0.
|
||||
12
Dockerfile
12
Dockerfile
@@ -1,20 +1,12 @@
|
||||
# +-+-+-+-+-+ +-+-+-+-+-+
|
||||
# |B|u|i|l|d| |S|t|a|g|e|
|
||||
# +-+-+-+-+-+ +-+-+-+-+-+
|
||||
# Build
|
||||
FROM node:lts-alpine as build-stage
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
# For testing purposes, it's easy to run http-server on lts-alpine such as continuing from here:
|
||||
# RUN npm install -g http-server
|
||||
# EXPOSE 8080
|
||||
# CMD [ "http-server", "dist" ]
|
||||
|
||||
# +-+-+-+-+-+-+-+-+-+-+ +-+-+-+-+-+
|
||||
# |P|r|o|d|u|c|t|i|o|n| |S|t|a|g|e|
|
||||
# +-+-+-+-+-+-+-+-+-+-+ +-+-+-+-+-+
|
||||
# Production stage
|
||||
FROM nginx:stable-alpine as production-stage
|
||||
COPY --from=build-stage /app/dist /usr/share/nginx/html
|
||||
EXPOSE 80
|
||||
|
||||
28
README.md
28
README.md
@@ -2,7 +2,7 @@
|
||||
|
||||
> Web tool to enforce privacy & security best-practices on Windows, because privacy is sexy 🍑🍆
|
||||
|
||||
[](https://github.com/undergroundwires/privacy.sexy/issues)
|
||||
[](./CONTRIBUTING.md)
|
||||
[](https://lgtm.com/projects/g/undergroundwires/privacy.sexy/context:javascript)
|
||||
[](https://codeclimate.com/github/undergroundwires/privacy.sexy/maintainability)
|
||||
[](https://github.com/undergroundwires/privacy.sexy/actions)
|
||||
@@ -16,28 +16,30 @@
|
||||
|
||||
## Why
|
||||
|
||||
- You don't need to run any compiled software on your system, just run the generated scripts.
|
||||
- You don't need to run any compiled software that has access to your system, just run the generated scripts.
|
||||
- It's open source, both application & infrastructure is 100% transparent
|
||||
- Fully automated C/CD pipeline to AWS for provisioning serverless infrastructure using GitHub actions.
|
||||
- Have full visibility into what the tweaks do as you enable them.
|
||||
- Ability to revert applied scripts
|
||||
- Easily extendable
|
||||
|
||||
## Extend scripts
|
||||
|
||||
Fork it & add more scripts in [application.yaml](src/application/application.yaml) and send a pull request 👌
|
||||
- Fork it & add more scripts in [application.yaml](src/application/application.yaml) and send a pull request 👌
|
||||
- 📖 More: [extend scripts | CONTRIBUTING.md](./CONTRIBUTING.md#extend-scripts)
|
||||
|
||||
## Commands
|
||||
|
||||
- Setup and run
|
||||
- For development:
|
||||
- `npm install` to project setup.
|
||||
- `npm run serve` to compile & hot-reload for development.
|
||||
- Production (using Docker):
|
||||
- Build `docker build -t undergroundwires/privacy.sexy .`
|
||||
- Run `docker run -it -p 8080:8080 --rm --name privacy.sexy-1 undergroundwires/privacy.sexy`
|
||||
- Prepare for production: `npm run build`
|
||||
- Run tests: `npm run test:unit`
|
||||
- Lint and fix files: `npm run lint`
|
||||
- Project setup: `npm install`
|
||||
- Testing
|
||||
- Run unit tests: `npm run test:unit`
|
||||
- Lint: `npm run lint`
|
||||
- **Webpage**
|
||||
- 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.4.10 .`
|
||||
2. Run: `docker run -it -p 8080:80 --rm --name privacy.sexy-0.4.10 undergroundwires/privacy.sexy:0.4.10`
|
||||
|
||||
## Architecture
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
BIN
docs/gitops.png
BIN
docs/gitops.png
Binary file not shown.
|
Before Width: | Height: | Size: 233 KiB After Width: | Height: | Size: 233 KiB |
27
package-lock.json
generated
27
package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "privacy.sexy",
|
||||
"version": "0.1.0",
|
||||
"version": "0.4.10",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@@ -6123,9 +6123,9 @@
|
||||
}
|
||||
},
|
||||
"lodash": {
|
||||
"version": "4.17.15",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
|
||||
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
|
||||
"version": "4.17.19",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
|
||||
"integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ=="
|
||||
},
|
||||
"lodash.defaultsdeep": {
|
||||
"version": "4.6.1",
|
||||
@@ -9792,6 +9792,11 @@
|
||||
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=",
|
||||
"dev": true
|
||||
},
|
||||
"resize-observer-polyfill": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
|
||||
"integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg=="
|
||||
},
|
||||
"resolve": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.14.1.tgz",
|
||||
@@ -12164,6 +12169,14 @@
|
||||
"integrity": "sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog==",
|
||||
"dev": true
|
||||
},
|
||||
"vue-js-modal": {
|
||||
"version": "2.0.0-rc.3",
|
||||
"resolved": "https://registry.npmjs.org/vue-js-modal/-/vue-js-modal-2.0.0-rc.3.tgz",
|
||||
"integrity": "sha512-Q9L9FsIUMuzCKSuB41D8LxV+Yc2q+zWHzzUdWaQ2KeHPS+78+X6AAnBztVoophbjt8UXHO7rQSRgER1MMw5qsw==",
|
||||
"requires": {
|
||||
"resize-observer-polyfill": "^1.5.1"
|
||||
}
|
||||
},
|
||||
"vue-loader": {
|
||||
"version": "15.8.3",
|
||||
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-15.8.3.tgz",
|
||||
@@ -12869,9 +12882,9 @@
|
||||
}
|
||||
},
|
||||
"websocket-extensions": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.3.tgz",
|
||||
"integrity": "sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==",
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz",
|
||||
"integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==",
|
||||
"dev": true
|
||||
},
|
||||
"whatwg-encoding": {
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
{
|
||||
"name": "privacy.sexy",
|
||||
"version": "0.4.3",
|
||||
"version": "0.4.10",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"test:unit": "vue-cli-service test:unit",
|
||||
"lint": "npm run lint:vue && npm run lint:yaml && npm run lint:md && npm run lint:md:relative-urls && npm run lint:md:consistency",
|
||||
"lint:vue": "vue-cli-service lint --no-fix",
|
||||
"lint:yaml": "yamllint **/*.yaml --ignore=node_modules/**/*.yaml",
|
||||
"test:unit": "vue-cli-service test:unit",
|
||||
"lint:md": "markdownlint **/*.md --ignore node_modules",
|
||||
"lint:md:relative-urls": "remark . --frail --use remark-validate-links",
|
||||
"lint:md:consistency": "remark . --frail --use remark-preset-lint-consistent"
|
||||
@@ -25,6 +26,7 @@
|
||||
"v-tooltip": "^2.0.2",
|
||||
"vue": "^2.6.11",
|
||||
"vue-class-component": "^7.1.0",
|
||||
"vue-js-modal": "^2.0.0-rc.3",
|
||||
"vue-property-decorator": "^8.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<TheSearchBar class="row" />
|
||||
<TheScripts class="row"/>
|
||||
<TheCodeArea class="row" theme="xcode" />
|
||||
<TheCodeButtons class="row" />
|
||||
<TheCodeButtons class="row code-buttons" />
|
||||
<TheFooter />
|
||||
</div>
|
||||
</div>
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop } from 'vue-property-decorator';
|
||||
import { ApplicationState, IApplicationState } from '@/application/State/ApplicationState';
|
||||
import { ApplicationState } from '@/application/State/ApplicationState';
|
||||
import TheHeader from '@/presentation/TheHeader.vue';
|
||||
import TheFooter from '@/presentation/TheFooter.vue';
|
||||
import TheCodeArea from '@/presentation/TheCodeArea.vue';
|
||||
@@ -67,6 +67,10 @@ body {
|
||||
.row {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.code-buttons {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,21 +1,29 @@
|
||||
import { Category } from '../../domain/Category';
|
||||
import { Application } from '../../domain/Application';
|
||||
import applicationFile from 'js-yaml-loader!./../application.yaml';
|
||||
import { Category } from '@/domain/Category';
|
||||
import { Application } from '@/domain/Application';
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
import { ApplicationYaml } from 'js-yaml-loader!./../application.yaml';
|
||||
import { parseCategory } from './CategoryParser';
|
||||
|
||||
export function parseApplication(): Application {
|
||||
export function parseApplication(content: ApplicationYaml): IApplication {
|
||||
validate(content);
|
||||
const categories = new Array<Category>();
|
||||
if (!applicationFile.actions || applicationFile.actions.length <= 0) {
|
||||
throw new Error('Application does not define any action');
|
||||
}
|
||||
for (const action of applicationFile.actions) {
|
||||
for (const action of content.actions) {
|
||||
const category = parseCategory(action);
|
||||
categories.push(category);
|
||||
}
|
||||
const app = new Application(
|
||||
applicationFile.name,
|
||||
applicationFile.repositoryUrl,
|
||||
content.name,
|
||||
content.repositoryUrl,
|
||||
process.env.VUE_APP_VERSION,
|
||||
categories);
|
||||
return app;
|
||||
}
|
||||
|
||||
function validate(content: ApplicationYaml): void {
|
||||
if (!content) {
|
||||
throw new Error('application is null or undefined');
|
||||
}
|
||||
if (!content.actions || content.actions.length <= 0) {
|
||||
throw new Error('application does not define any action');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
import { YamlCategory, YamlScript } from 'js-yaml-loader!./application.yaml';
|
||||
import { Script } from '@/domain/Script';
|
||||
import { Category } from '../../domain/Category';
|
||||
import { Category } from '@/domain/Category';
|
||||
import { parseDocUrls } from './DocumentationParser';
|
||||
import { parseScript } from './ScriptParser';
|
||||
|
||||
let categoryIdCounter: number = 0;
|
||||
|
||||
|
||||
interface ICategoryChildren {
|
||||
subCategories: Category[];
|
||||
subScripts: Script[];
|
||||
}
|
||||
|
||||
export function parseCategory(category: YamlCategory): Category {
|
||||
if (!category.children || category.children.length <= 0) {
|
||||
throw Error('Category has no children');
|
||||
}
|
||||
ensureValid(category);
|
||||
const children: ICategoryChildren = {
|
||||
subCategories: new Array<Category>(),
|
||||
subScripts: new Array<Script>(),
|
||||
@@ -31,6 +29,18 @@ export function parseCategory(category: YamlCategory): Category {
|
||||
);
|
||||
}
|
||||
|
||||
function ensureValid(category: YamlCategory) {
|
||||
if (!category) {
|
||||
throw Error('category is null or undefined');
|
||||
}
|
||||
if (!category.children || category.children.length === 0) {
|
||||
throw Error('category has no children');
|
||||
}
|
||||
if (!category.category || category.category.length === 0) {
|
||||
throw Error('category has no name');
|
||||
}
|
||||
}
|
||||
|
||||
function parseCategoryChild(
|
||||
categoryOrScript: any, children: ICategoryChildren, parent: YamlCategory) {
|
||||
if (isCategory(categoryOrScript)) {
|
||||
@@ -38,11 +48,7 @@ function parseCategoryChild(
|
||||
children.subCategories.push(subCategory);
|
||||
} else if (isScript(categoryOrScript)) {
|
||||
const yamlScript = categoryOrScript as YamlScript;
|
||||
const script = new Script(
|
||||
/* name */ yamlScript.name,
|
||||
/* code */ yamlScript.code,
|
||||
/* docs */ parseDocUrls(yamlScript),
|
||||
/* is recommended? */ yamlScript.recommend);
|
||||
const script = parseScript(yamlScript);
|
||||
children.subScripts.push(script);
|
||||
} else {
|
||||
throw new Error(`Child element is neither a category or a script.
|
||||
@@ -50,7 +56,6 @@ function parseCategoryChild(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function isScript(categoryOrScript: any): boolean {
|
||||
return categoryOrScript.code && categoryOrScript.code.length > 0;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { YamlDocumentable } from 'js-yaml-loader!./application.yaml';
|
||||
|
||||
export function parseDocUrls(documentable: YamlDocumentable): ReadonlyArray<string> {
|
||||
if (!documentable) {
|
||||
throw new Error('documentable is null or undefined');
|
||||
}
|
||||
const docs = documentable.docs;
|
||||
if (!docs) {
|
||||
return [];
|
||||
|
||||
16
src/application/Parser/ScriptParser.ts
Normal file
16
src/application/Parser/ScriptParser.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Script } from '@/domain/Script';
|
||||
import { YamlScript } from 'js-yaml-loader!./application.yaml';
|
||||
import { parseDocUrls } from './DocumentationParser';
|
||||
|
||||
export function parseScript(yamlScript: YamlScript): Script {
|
||||
if (!yamlScript) {
|
||||
throw new Error('script is null or undefined');
|
||||
}
|
||||
const script = new Script(
|
||||
/* name */ yamlScript.name,
|
||||
/* code */ yamlScript.code,
|
||||
/* revertCode */ yamlScript.revertCode,
|
||||
/* docs */ parseDocUrls(yamlScript),
|
||||
/* isRecommended */ yamlScript.recommend);
|
||||
return script;
|
||||
}
|
||||
@@ -3,13 +3,14 @@ import { IUserFilter } from './Filter/IUserFilter';
|
||||
import { ApplicationCode } from './Code/ApplicationCode';
|
||||
import { UserSelection } from './Selection/UserSelection';
|
||||
import { IUserSelection } from './Selection/IUserSelection';
|
||||
import { AsyncLazy } from '../../infrastructure/Threading/AsyncLazy';
|
||||
import { Signal } from '../../infrastructure/Events/Signal';
|
||||
import { AsyncLazy } from '@/infrastructure/Threading/AsyncLazy';
|
||||
import { Signal } from '@/infrastructure/Events/Signal';
|
||||
import { parseApplication } from '../Parser/ApplicationParser';
|
||||
import { IApplicationState } from './IApplicationState';
|
||||
import { Script } from '../../domain/Script';
|
||||
import { Application } from '../../domain/Application';
|
||||
import { Script } from '@/domain/Script';
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
import { IApplicationCode } from './Code/IApplicationCode';
|
||||
import applicationFile from 'js-yaml-loader!@/application/application.yaml';
|
||||
|
||||
/** Mutatable singleton application state that's the single source of truth throughout the application */
|
||||
export class ApplicationState implements IApplicationState {
|
||||
@@ -20,7 +21,7 @@ export class ApplicationState implements IApplicationState {
|
||||
|
||||
/** Application instance with all scripts. */
|
||||
private static instance = new AsyncLazy<IApplicationState>(() => {
|
||||
const application = parseApplication();
|
||||
const application = parseApplication(applicationFile);
|
||||
const selectedScripts = new Array<Script>();
|
||||
const state = new ApplicationState(application, selectedScripts);
|
||||
return Promise.resolve(state);
|
||||
@@ -33,7 +34,7 @@ export class ApplicationState implements IApplicationState {
|
||||
|
||||
private constructor(
|
||||
/** Inner instance of the all scripts */
|
||||
public readonly app: Application,
|
||||
public readonly app: IApplication,
|
||||
/** Initially selected scripts */
|
||||
public readonly defaultScripts: Script[]) {
|
||||
this.selection = new UserSelection(app, defaultScripts);
|
||||
@@ -41,5 +42,3 @@ export class ApplicationState implements IApplicationState {
|
||||
this.filter = new UserFilter(app);
|
||||
}
|
||||
}
|
||||
|
||||
export { IApplicationState, IUserFilter };
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
||||
import { IUserSelection } from '@/application/State/Selection/IUserSelection';
|
||||
import { UserScriptGenerator } from './UserScriptGenerator';
|
||||
import { IUserSelection } from './../Selection/IUserSelection';
|
||||
import { Signal } from '@/infrastructure/Events/Signal';
|
||||
import { IApplicationCode } from './IApplicationCode';
|
||||
import { IScript } from '@/domain/IScript';
|
||||
import { IUserScriptGenerator } from './IUserScriptGenerator';
|
||||
|
||||
export class ApplicationCode implements IApplicationCode {
|
||||
public readonly changed = new Signal<string>();
|
||||
public current: string;
|
||||
|
||||
private readonly generator: UserScriptGenerator;
|
||||
private readonly generator: IUserScriptGenerator = new UserScriptGenerator();
|
||||
|
||||
constructor(userSelection: IUserSelection, private readonly version: string) {
|
||||
constructor(
|
||||
userSelection: IUserSelection,
|
||||
private readonly version: string) {
|
||||
if (!userSelection) { throw new Error('userSelection is null or undefined'); }
|
||||
if (!version) { throw new Error('version is null or undefined'); }
|
||||
this.generator = new UserScriptGenerator();
|
||||
@@ -20,7 +23,7 @@ export class ApplicationCode implements IApplicationCode {
|
||||
});
|
||||
}
|
||||
|
||||
private setCode(scripts: ReadonlyArray<IScript>) {
|
||||
private setCode(scripts: ReadonlyArray<SelectedScript>) {
|
||||
this.current = scripts.length === 0 ? '' : this.generator.buildCode(scripts, this.version);
|
||||
this.changed.notify(this.current);
|
||||
}
|
||||
|
||||
5
src/application/State/Code/IUserScriptGenerator.ts
Normal file
5
src/application/State/Code/IUserScriptGenerator.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
||||
|
||||
export interface IUserScriptGenerator {
|
||||
buildCode(selectedScripts: ReadonlyArray<SelectedScript>, version: string): string;
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
||||
import { IUserScriptGenerator } from './IUserScriptGenerator';
|
||||
import { CodeBuilder } from './CodeBuilder';
|
||||
import { Script } from '@/domain/Script';
|
||||
|
||||
const adminRightsScript = {
|
||||
export const adminRightsScript = {
|
||||
name: 'Ensure admin privileges',
|
||||
code: 'fltmc >nul 2>&1 || (\n' +
|
||||
' echo This batch script requires administrator privileges. Right-click on\n' +
|
||||
@@ -11,17 +12,19 @@ const adminRightsScript = {
|
||||
')',
|
||||
};
|
||||
|
||||
export class UserScriptGenerator {
|
||||
public buildCode(scripts: ReadonlyArray<Script>, version: string): string {
|
||||
if (!scripts) { throw new Error('scripts is undefined'); }
|
||||
if (!scripts.length) { throw new Error('scripts are empty'); }
|
||||
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 script of scripts) {
|
||||
builder.appendFunction(script.name, script.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')
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { IFilterResult } from './IFilterResult';
|
||||
import { IScript } from '@/domain/Script';
|
||||
import { IScript } from '@/domain/IScript';
|
||||
import { ICategory } from '@/domain/ICategory';
|
||||
|
||||
export class FilterResult implements IFilterResult {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { IScript } from '@/domain/IScript';
|
||||
import { FilterResult } from './FilterResult';
|
||||
import { IFilterResult } from './IFilterResult';
|
||||
import { Application } from '../../../domain/Application';
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
import { IUserFilter } from './IUserFilter';
|
||||
import { Signal } from '@/infrastructure/Events/Signal';
|
||||
|
||||
@@ -8,7 +9,7 @@ export class UserFilter implements IUserFilter {
|
||||
public readonly filtered = new Signal<IFilterResult>();
|
||||
public readonly filterRemoved = new Signal<void>();
|
||||
|
||||
constructor(private application: Application) {
|
||||
constructor(private application: IApplication) {
|
||||
|
||||
}
|
||||
|
||||
@@ -18,11 +19,9 @@ export class UserFilter implements IUserFilter {
|
||||
}
|
||||
const filterLowercase = filter.toLocaleLowerCase();
|
||||
const filteredScripts = this.application.getAllScripts().filter(
|
||||
(script) =>
|
||||
script.name.toLowerCase().includes(filterLowercase) ||
|
||||
script.code.toLowerCase().includes(filterLowercase));
|
||||
(script) => isScriptAMatch(script, filterLowercase));
|
||||
const filteredCategories = this.application.getAllCategories().filter(
|
||||
(script) => script.name.toLowerCase().includes(filterLowercase));
|
||||
(category) => category.name.toLowerCase().includes(filterLowercase));
|
||||
|
||||
const matches = new FilterResult(
|
||||
filteredScripts,
|
||||
@@ -37,3 +36,16 @@ export class UserFilter implements IUserFilter {
|
||||
this.filterRemoved.notify();
|
||||
}
|
||||
}
|
||||
|
||||
function isScriptAMatch(script: IScript, filterLowercase: string) {
|
||||
if (script.name.toLowerCase().includes(filterLowercase)) {
|
||||
return true;
|
||||
}
|
||||
if (script.code.toLowerCase().includes(filterLowercase)) {
|
||||
return true;
|
||||
}
|
||||
if (script.revertCode) {
|
||||
return script.revertCode.toLowerCase().includes(filterLowercase);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { SelectedScript } from './SelectedScript';
|
||||
import { ISignal } from '@/infrastructure/Events/Signal';
|
||||
import { IScript } from '@/domain/IScript';
|
||||
|
||||
export interface IUserSelection {
|
||||
readonly changed: ISignal<ReadonlyArray<IScript>>;
|
||||
readonly selectedScripts: ReadonlyArray<IScript>;
|
||||
readonly changed: ISignal<ReadonlyArray<SelectedScript>>;
|
||||
readonly selectedScripts: ReadonlyArray<SelectedScript>;
|
||||
readonly totalSelected: number;
|
||||
addSelectedScript(scriptId: string): 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;
|
||||
|
||||
14
src/application/State/Selection/SelectedScript.ts
Normal file
14
src/application/State/Selection/SelectedScript.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
|
||||
import { IScript } from '@/domain/IScript';
|
||||
|
||||
export class SelectedScript extends BaseEntity<string> {
|
||||
constructor(
|
||||
public readonly script: IScript,
|
||||
public readonly revert: boolean,
|
||||
) {
|
||||
super(script.id);
|
||||
if (revert && !script.canRevert()) {
|
||||
throw new Error('cannot revert an irreversible script');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
import { SelectedScript } from './SelectedScript';
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
import { IUserSelection } from './IUserSelection';
|
||||
import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository';
|
||||
import { IScript } from '@/domain/Script';
|
||||
import { IScript } from '@/domain/IScript';
|
||||
import { Signal } from '@/infrastructure/Events/Signal';
|
||||
import { IRepository } from '@/infrastructure/Repository/IRepository';
|
||||
|
||||
export class UserSelection implements IUserSelection {
|
||||
public readonly changed = new Signal<ReadonlyArray<IScript>>();
|
||||
|
||||
private readonly scripts = new InMemoryRepository<string, IScript>();
|
||||
public readonly changed = new Signal<ReadonlyArray<SelectedScript>>();
|
||||
private readonly scripts: IRepository<string, SelectedScript> = new InMemoryRepository<string, SelectedScript>();
|
||||
|
||||
constructor(
|
||||
private readonly app: IApplication,
|
||||
@@ -15,33 +16,40 @@ export class UserSelection implements IUserSelection {
|
||||
selectedScripts: ReadonlyArray<IScript>) {
|
||||
if (selectedScripts && selectedScripts.length > 0) {
|
||||
for (const script of selectedScripts) {
|
||||
this.scripts.addItem(script);
|
||||
const selected = new SelectedScript(script, false);
|
||||
this.scripts.addItem(selected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Add a script to users application */
|
||||
public addSelectedScript(scriptId: string): void {
|
||||
public addSelectedScript(scriptId: string, revert: boolean): void {
|
||||
const script = this.app.findScript(scriptId);
|
||||
if (!script) {
|
||||
throw new Error(`Cannot add (id: ${scriptId}) as it is unknown`);
|
||||
}
|
||||
this.scripts.addItem(script);
|
||||
const selectedScript = new SelectedScript(script, revert);
|
||||
this.scripts.addItem(selectedScript);
|
||||
this.changed.notify(this.scripts.getItems());
|
||||
}
|
||||
|
||||
public addOrUpdateSelectedScript(scriptId: string, revert: boolean): void {
|
||||
const script = this.app.findScript(scriptId);
|
||||
const selectedScript = new SelectedScript(script, revert);
|
||||
this.scripts.addOrUpdateItem(selectedScript);
|
||||
this.changed.notify(this.scripts.getItems());
|
||||
}
|
||||
|
||||
/** Remove a script from users application */
|
||||
public removeSelectedScript(scriptId: string): void {
|
||||
this.scripts.removeItem(scriptId);
|
||||
this.changed.notify(this.scripts.getItems());
|
||||
}
|
||||
|
||||
public isSelected(script: IScript): boolean {
|
||||
return this.scripts.exists(script);
|
||||
return this.scripts.exists(script.id);
|
||||
}
|
||||
|
||||
/** Get users scripts based on his/her selections */
|
||||
public get selectedScripts(): ReadonlyArray<IScript> {
|
||||
public get selectedScripts(): ReadonlyArray<SelectedScript> {
|
||||
return this.scripts.getItems();
|
||||
}
|
||||
|
||||
@@ -51,8 +59,9 @@ export class UserSelection implements IUserSelection {
|
||||
|
||||
public selectAll(): void {
|
||||
for (const script of this.app.getAllScripts()) {
|
||||
if (!this.scripts.exists(script)) {
|
||||
this.scripts.addItem(script);
|
||||
if (!this.scripts.exists(script.id)) {
|
||||
const selection = new SelectedScript(script, false);
|
||||
this.scripts.addItem(selection);
|
||||
}
|
||||
}
|
||||
this.changed.notify(this.scripts.getItems());
|
||||
@@ -78,9 +87,11 @@ export class UserSelection implements IUserSelection {
|
||||
.forEach((scriptId) => this.scripts.removeItem(scriptId));
|
||||
}
|
||||
// Select from unselected scripts
|
||||
scripts
|
||||
.filter((script) => !this.scripts.exists(script))
|
||||
.forEach((script) => this.scripts.addItem(script));
|
||||
const unselectedScripts = scripts.filter((script) => !this.scripts.exists(script.id));
|
||||
for (const toSelect of unselectedScripts) {
|
||||
const selection = new SelectedScript(toSelect, false);
|
||||
this.scripts.addItem(selection);
|
||||
}
|
||||
this.changed.notify(this.scripts.getItems());
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
5
src/application/application.yaml.d.ts
vendored
5
src/application/application.yaml.d.ts
vendored
@@ -1,5 +1,5 @@
|
||||
declare module 'js-yaml-loader!*' {
|
||||
type CategoryOrScript = YamlCategory | YamlScript;
|
||||
export type CategoryOrScript = YamlCategory | YamlScript;
|
||||
type DocumentationUrls = ReadonlyArray<string> | string;
|
||||
|
||||
export interface YamlDocumentable {
|
||||
@@ -9,6 +9,7 @@ declare module 'js-yaml-loader!*' {
|
||||
export interface YamlScript extends YamlDocumentable {
|
||||
name: string;
|
||||
code: string;
|
||||
revertCode: string;
|
||||
recommend: boolean;
|
||||
}
|
||||
|
||||
@@ -17,7 +18,7 @@ declare module 'js-yaml-loader!*' {
|
||||
category: string;
|
||||
}
|
||||
|
||||
interface ApplicationYaml {
|
||||
export interface ApplicationYaml {
|
||||
name: string;
|
||||
repositoryUrl: string;
|
||||
actions: ReadonlyArray<YamlCategory>;
|
||||
|
||||
@@ -13,11 +13,11 @@ export class Application implements IApplication {
|
||||
public readonly name: string,
|
||||
public readonly repositoryUrl: string,
|
||||
public readonly version: string,
|
||||
public readonly categories: ReadonlyArray<ICategory>) {
|
||||
public readonly actions: ReadonlyArray<ICategory>) {
|
||||
if (!name) { throw Error('Application has no name'); }
|
||||
if (!repositoryUrl) { throw Error('Application has no repository url'); }
|
||||
if (!version) { throw Error('Version cannot be empty'); }
|
||||
this.flattened = flatten(categories);
|
||||
this.flattened = flatten(actions);
|
||||
if (this.flattened.allCategories.length === 0) {
|
||||
throw new Error('Application must consist of at least one category');
|
||||
}
|
||||
|
||||
@@ -5,9 +5,9 @@ export interface IApplication {
|
||||
readonly name: string;
|
||||
readonly repositoryUrl: string;
|
||||
readonly version: string;
|
||||
readonly categories: ReadonlyArray<ICategory>;
|
||||
readonly totalScripts: number;
|
||||
readonly totalCategories: number;
|
||||
readonly actions: ReadonlyArray<ICategory>;
|
||||
|
||||
getRecommendedScripts(): ReadonlyArray<IScript>;
|
||||
findCategory(categoryId: number): ICategory | undefined;
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { IEntity } from './../infrastructure/Entity/IEntity';
|
||||
import { IEntity } from '../infrastructure/Entity/IEntity';
|
||||
import { IDocumentable } from './IDocumentable';
|
||||
|
||||
export interface IScript extends IEntity<string>, IDocumentable {
|
||||
readonly name: string;
|
||||
readonly code: string;
|
||||
readonly isRecommended: boolean;
|
||||
readonly documentationUrls: ReadonlyArray<string>;
|
||||
readonly code: string;
|
||||
readonly revertCode: string;
|
||||
canRevert(): boolean;
|
||||
}
|
||||
|
||||
@@ -2,44 +2,56 @@ import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
|
||||
import { IScript } from './IScript';
|
||||
|
||||
export class Script extends BaseEntity<string> implements IScript {
|
||||
private static ensureNoEmptyLines(name: string, code: string): void {
|
||||
if (code.split('\n').some((line) => line.trim().length === 0)) {
|
||||
throw Error(`Script has empty lines "${name}"`);
|
||||
}
|
||||
}
|
||||
|
||||
private static ensureCodeHasUniqueLines(name: string, code: string): void {
|
||||
const lines = code.split('\n')
|
||||
.filter((line) => this.mayBeUniqueLine(line));
|
||||
if (lines.length === 0) {
|
||||
return;
|
||||
}
|
||||
const duplicateLines = lines.filter((e, i, a) => a.indexOf(e) !== i);
|
||||
if (duplicateLines.length !== 0) {
|
||||
throw Error(`Duplicates detected in script "${name}":\n ${duplicateLines.join('\n')}`);
|
||||
}
|
||||
}
|
||||
|
||||
private static mayBeUniqueLine(codeLine: string): boolean {
|
||||
const trimmed = codeLine.trim();
|
||||
if (trimmed === ')' || trimmed === '(') { // "(" and ")" are used often in batch code
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
constructor(
|
||||
public name: string,
|
||||
public code: string,
|
||||
public documentationUrls: ReadonlyArray<string>,
|
||||
public isRecommended: boolean) {
|
||||
public readonly name: string,
|
||||
public readonly code: string,
|
||||
public readonly revertCode: string,
|
||||
public readonly documentationUrls: ReadonlyArray<string>,
|
||||
public readonly isRecommended: boolean) {
|
||||
super(name);
|
||||
if (code == null || code.length === 0) {
|
||||
throw new Error('Code is empty or null');
|
||||
validateCode(name, code);
|
||||
if (revertCode) {
|
||||
validateCode(name, revertCode);
|
||||
if (code === revertCode) {
|
||||
throw new Error(`${name}: Code itself and its reverting code cannot be the same`);
|
||||
}
|
||||
}
|
||||
Script.ensureCodeHasUniqueLines(name, code);
|
||||
Script.ensureNoEmptyLines(name, code);
|
||||
}
|
||||
public canRevert(): boolean {
|
||||
return Boolean(this.revertCode);
|
||||
}
|
||||
}
|
||||
|
||||
export { IScript } from './IScript';
|
||||
function validateCode(name: string, code: string): void {
|
||||
if (!code || code.length === 0) {
|
||||
throw new Error(`Code of ${name} is empty or null`);
|
||||
}
|
||||
ensureCodeHasUniqueLines(name, code);
|
||||
ensureNoEmptyLines(name, code);
|
||||
}
|
||||
|
||||
function ensureNoEmptyLines(name: string, code: string): void {
|
||||
if (code.split('\n').some((line) => line.trim().length === 0)) {
|
||||
throw Error(`Script has empty lines "${name}"`);
|
||||
}
|
||||
}
|
||||
|
||||
function mayBeUniqueLine(codeLine: string): boolean {
|
||||
const trimmed = codeLine.trim();
|
||||
if (trimmed === ')' || trimmed === '(') { // "(" and ")" are used often in batch code
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function ensureCodeHasUniqueLines(name: string, code: string): void {
|
||||
const lines = code.split('\n')
|
||||
.filter((line) => mayBeUniqueLine(line));
|
||||
if (lines.length === 0) {
|
||||
return;
|
||||
}
|
||||
const duplicateLines = lines.filter((e, i, a) => a.indexOf(e) !== i);
|
||||
if (duplicateLines.length !== 0) {
|
||||
throw Error(`Duplicates detected in script "${name}":\n ${duplicateLines.join('\n')}`);
|
||||
}
|
||||
}
|
||||
|
||||
1
src/global.d.ts
vendored
1
src/global.d.ts
vendored
@@ -13,6 +13,7 @@ declare module 'liquor-tree' {
|
||||
}
|
||||
interface ICustomLiquorTreeData {
|
||||
documentationUrls: ReadonlyArray<string>;
|
||||
isReversible: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,6 +4,7 @@ export interface IRepository<TKey, TEntity extends IEntity<TKey>> {
|
||||
readonly length: number;
|
||||
getItems(predicate?: (entity: TEntity) => boolean): TEntity[];
|
||||
addItem(item: TEntity): void;
|
||||
addOrUpdateItem(item: TEntity): void;
|
||||
removeItem(id: TKey): void;
|
||||
exists(item: TEntity): boolean;
|
||||
exists(id: TKey): boolean;
|
||||
}
|
||||
|
||||
@@ -18,14 +18,24 @@ export class InMemoryRepository<TKey, TEntity extends IEntity<TKey>> implements
|
||||
|
||||
public addItem(item: TEntity): void {
|
||||
if (!item) {
|
||||
throw new Error('Item is null');
|
||||
throw new Error('item is null or undefined');
|
||||
}
|
||||
if (this.exists(item)) {
|
||||
if (this.exists(item.id)) {
|
||||
throw new Error(`Cannot add (id: ${item.id}) as it is already exists`);
|
||||
}
|
||||
this.items.push(item);
|
||||
}
|
||||
|
||||
public addOrUpdateItem(item: TEntity): void {
|
||||
if (!item) {
|
||||
throw new Error('item is null or undefined');
|
||||
}
|
||||
if (this.exists(item.id)) {
|
||||
this.removeItem(item.id);
|
||||
}
|
||||
this.items.push(item);
|
||||
}
|
||||
|
||||
public removeItem(id: TKey): void {
|
||||
const index = this.items.findIndex((item) => item.id === id);
|
||||
if (index === -1) {
|
||||
@@ -34,8 +44,8 @@ export class InMemoryRepository<TKey, TEntity extends IEntity<TKey>> implements
|
||||
this.items.splice(index, 1);
|
||||
}
|
||||
|
||||
public exists(entity: TEntity): boolean {
|
||||
const index = this.items.findIndex((item) => item.id === entity.id);
|
||||
public exists(id: TKey): boolean {
|
||||
const index = this.items.findIndex((item) => item.id === id);
|
||||
return index !== -1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { VModalBootstrapper } from './Modules/VModalBootstrapper';
|
||||
import { TreeBootstrapper } from './Modules/TreeBootstrapper';
|
||||
import { IconBootstrapper } from './Modules/IconBootstrapper';
|
||||
import { VueConstructor, IVueBootstrapper } from './IVueBootstrapper';
|
||||
@@ -19,6 +20,7 @@ export class ApplicationBootstrapper implements IVueBootstrapper {
|
||||
new TreeBootstrapper(),
|
||||
new VueBootstrapper(),
|
||||
new TooltipBootstrapper(),
|
||||
new VModalBootstrapper(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import VModal from 'vue-js-modal';
|
||||
import { VueConstructor, IVueBootstrapper } from './../IVueBootstrapper';
|
||||
|
||||
export class VModalBootstrapper implements IVueBootstrapper {
|
||||
public bootstrap(vue: VueConstructor): void {
|
||||
vue.use(VModal, { dynamic: true, injectModalsContainer: true });
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue, Emit } from 'vue-property-decorator';
|
||||
import { StatefulVue, IApplicationState } from './StatefulVue';
|
||||
import { StatefulVue } from './StatefulVue';
|
||||
import { SaveFileDialog } from './../infrastructure/SaveFileDialog';
|
||||
import { Clipboard } from './../infrastructure/Clipboard';
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<CardListItem
|
||||
class="card"
|
||||
v-for="categoryId of categoryIds"
|
||||
:data-category="categoryId"
|
||||
v-bind:key="categoryId"
|
||||
:categoryId="categoryId"
|
||||
:activeCategoryId="activeCategoryId"
|
||||
@@ -17,8 +18,9 @@
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import CardListItem from './CardListItem.vue';
|
||||
import { StatefulVue, IApplicationState } from '@/presentation/StatefulVue';
|
||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||
import { ICategory } from '@/domain/ICategory';
|
||||
import { hasDirective } from './NonCollapsingDirective';
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
@@ -31,7 +33,13 @@ export default class CardList extends StatefulVue {
|
||||
|
||||
public async mounted() {
|
||||
const state = await this.getCurrentStateAsync();
|
||||
this.setCategories(state.app.categories);
|
||||
this.setCategories(state.app.actions);
|
||||
this.onOutsideOfActiveCardClicked((element) => {
|
||||
if (hasDirective(element)) {
|
||||
return;
|
||||
}
|
||||
this.activeCategoryId = null;
|
||||
});
|
||||
}
|
||||
|
||||
public onSelected(categoryId: number, isExpanded: boolean) {
|
||||
@@ -41,7 +49,21 @@ export default class CardList extends StatefulVue {
|
||||
private setCategories(categories: ReadonlyArray<ICategory>): void {
|
||||
this.categoryIds = categories.map((category) => category.id);
|
||||
}
|
||||
|
||||
private onOutsideOfActiveCardClicked(callback: (clickedElement: Element) => void) {
|
||||
const outsideClickListener = (event) => {
|
||||
if (!this.activeCategoryId) {
|
||||
return;
|
||||
}
|
||||
const element = document.querySelector(`[data-category="${this.activeCategoryId}"]`);
|
||||
if (element && !element.contains(event.target)) {
|
||||
callback(event.target);
|
||||
}
|
||||
};
|
||||
document.addEventListener('click', outsideClickListener);
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -4,16 +4,22 @@
|
||||
v-bind:class="{
|
||||
'is-collapsed': !isExpanded,
|
||||
'is-inactive': activeCategoryId && activeCategoryId != categoryId,
|
||||
'is-expanded': isExpanded}">
|
||||
<div class="card__inner">
|
||||
<span v-if="cardTitle && cardTitle.length > 0">{{cardTitle}}</span>
|
||||
<span v-else>Oh no 😢</span>
|
||||
<font-awesome-icon :icon="['far', isExpanded ? 'folder-open' : 'folder']" class="expand-button" />
|
||||
'is-expanded': isExpanded
|
||||
}"
|
||||
ref="cardElement">
|
||||
<div class="card__inner">
|
||||
<span v-if="cardTitle && cardTitle.length > 0">{{cardTitle}}</span>
|
||||
<span v-else>Oh no 😢</span>
|
||||
<font-awesome-icon :icon="['far', isExpanded ? 'folder-open' : 'folder']" class="card__inner__expand-icon" />
|
||||
</div>
|
||||
<div class="card__expander" v-on:click.stop>
|
||||
<div class="card__expander__content">
|
||||
<ScriptsTree :categoryId="categoryId"></ScriptsTree>
|
||||
</div>
|
||||
<div class="card__expander__close-button">
|
||||
<font-awesome-icon :icon="['fas', 'times']" v-on:click="onSelected(false)"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card__expander" v-on:click.stop>
|
||||
<font-awesome-icon :icon="['fas', 'times']" class="close-button" v-on:click="onSelected(false)"/>
|
||||
<ScriptsTree :categoryId="categoryId"></ScriptsTree>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -37,11 +43,18 @@ export default class CardListItem extends StatefulVue {
|
||||
public onSelected(isExpanded: boolean) {
|
||||
this.isExpanded = isExpanded;
|
||||
}
|
||||
|
||||
@Watch('activeCategoryId')
|
||||
public async onActiveCategoryChanged(value: |number) {
|
||||
this.isExpanded = value === this.categoryId;
|
||||
}
|
||||
@Watch('isExpanded')
|
||||
public async onExpansionChangedAsync(newValue: number, oldValue: number) {
|
||||
if (!oldValue && newValue) {
|
||||
await new Promise((r) => setTimeout(r, 400));
|
||||
const focusElement = this.$refs.cardElement as HTMLElement;
|
||||
(focusElement as HTMLElement).scrollIntoView({behavior: 'smooth'});
|
||||
}
|
||||
}
|
||||
|
||||
public async mounted() {
|
||||
this.cardTitle = this.categoryId ? await this.getCardTitleAsync(this.categoryId) : undefined;
|
||||
@@ -63,73 +76,76 @@ export default class CardListItem extends StatefulVue {
|
||||
<style scoped lang="scss">
|
||||
@import "@/presentation/styles/colors.scss";
|
||||
|
||||
$big-screen-width: 991px;
|
||||
$medium-screen-width: 767px;
|
||||
$small-screen-width: 380px;
|
||||
|
||||
$card-padding: 30px;
|
||||
$card-margin: 15px;
|
||||
$card-line-break-width: 30px;
|
||||
$arrow-size: 15px;
|
||||
$expanded-margin-top: 30px;
|
||||
|
||||
.card {
|
||||
margin: 15px;
|
||||
width: calc((100% / 3) - 30px);
|
||||
width: calc((100% / 3) - #{$card-line-break-width});
|
||||
transition: all 0.2s ease-in-out;
|
||||
// Media queries for stacking cards
|
||||
@media screen and (max-width: $big-screen-width) { width: calc((100% / 2) - #{$card-line-break-width}); }
|
||||
@media screen and (max-width: $medium-screen-width) { width: 100%; }
|
||||
@media screen and (max-width: $small-screen-width) { width: 90%; }
|
||||
|
||||
//media queries for stacking cards
|
||||
@media screen and (max-width: 991px) {
|
||||
width: calc((100% / 2) - 30px);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 767px) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 380px) {
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.card__inner {
|
||||
background-color: $accent;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
&__inner {
|
||||
width: 100%;
|
||||
padding: 30px;
|
||||
&__inner {
|
||||
padding: $card-padding;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
background-color: $gray;
|
||||
color: $light-gray;
|
||||
font-size: 1.5em;
|
||||
height: 100%;
|
||||
text-transform: uppercase;
|
||||
text-align: center;
|
||||
|
||||
transition: all 0.2s ease-in-out;
|
||||
|
||||
|
||||
display:flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
&:hover {
|
||||
background-color: $accent;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
&:after {
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.expand-button {
|
||||
|
||||
&__expand-icon {
|
||||
width: 100%;
|
||||
margin-top: .25em;
|
||||
}
|
||||
}
|
||||
|
||||
//Expander
|
||||
&__expander {
|
||||
transition: all 0.2s ease-in-out;
|
||||
background-color: $slate;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
|
||||
background-color: $slate;
|
||||
color: $light-gray;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&__content {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
font-size: 0.75em;
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
&__close-button {
|
||||
width: auto;
|
||||
font-size: 1.5em;
|
||||
align-self: flex-start;
|
||||
margin-right:0.25em;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
@@ -137,49 +153,42 @@ export default class CardListItem extends StatefulVue {
|
||||
}
|
||||
|
||||
&.is-collapsed {
|
||||
|
||||
.card__inner {
|
||||
&:after {
|
||||
content: "";
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.card__expander {
|
||||
max-height: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
margin-top: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-expanded {
|
||||
|
||||
.card__inner {
|
||||
height: auto;
|
||||
background-color: $accent;
|
||||
|
||||
&:after{
|
||||
&:after { // arrow
|
||||
content: "";
|
||||
opacity: 1;
|
||||
display: block;
|
||||
height: 0;
|
||||
width: 0;
|
||||
position: absolute;
|
||||
bottom: -30px;
|
||||
left: calc(50% - 15px);
|
||||
border-left: 15px solid transparent;
|
||||
border-right: 15px solid transparent;
|
||||
border-bottom: 15px solid #333a45;
|
||||
bottom: calc(-1 * #{$expanded-margin-top});
|
||||
left: calc(50% - #{$arrow-size});
|
||||
border-left: #{$arrow-size} solid transparent;
|
||||
border-right: #{$arrow-size} solid transparent;
|
||||
border-bottom: #{$arrow-size} solid #333a45;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.card__expander {
|
||||
min-height: 200px;
|
||||
// max-height: 1000px;
|
||||
// overflow-y: auto;
|
||||
|
||||
margin-top: 30px;
|
||||
margin-top: $expanded-margin-top;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@@ -193,7 +202,9 @@ export default class CardListItem extends StatefulVue {
|
||||
&.is-inactive {
|
||||
.card__inner {
|
||||
pointer-events: none;
|
||||
height: auto;
|
||||
opacity: 0.5;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@@ -205,39 +216,30 @@ export default class CardListItem extends StatefulVue {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//Expander Widths
|
||||
|
||||
//when 3 cards in a row
|
||||
@media screen and (min-width: 992px) {
|
||||
|
||||
@media screen and (min-width: $big-screen-width) { // when 3 cards in a row
|
||||
.card:nth-of-type(3n+2) .card__expander {
|
||||
margin-left: calc(-100% - 30px);
|
||||
margin-left: calc(-100% - #{$card-line-break-width});
|
||||
}
|
||||
.card:nth-of-type(3n+3) .card__expander {
|
||||
margin-left: calc(-200% - 60px);
|
||||
margin-left: calc(-200% - (#{$card-line-break-width} * 2));
|
||||
}
|
||||
.card:nth-of-type(3n+4) {
|
||||
clear: left;
|
||||
}
|
||||
.card__expander {
|
||||
width: calc(300% + 60px);
|
||||
width: calc(300% + (#{$card-line-break-width} * 2));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//when 2 cards in a row
|
||||
@media screen and (min-width: 768px) and (max-width: 991px) {
|
||||
|
||||
@media screen and (min-width: $medium-screen-width) and (max-width: $big-screen-width) { // when 2 cards in a row
|
||||
.card:nth-of-type(2n+2) .card__expander {
|
||||
margin-left: calc(-100% - 30px);
|
||||
margin-left: calc(-100% - #{$card-line-break-width});
|
||||
}
|
||||
.card:nth-of-type(2n+3) {
|
||||
clear: left;
|
||||
}
|
||||
.card__expander {
|
||||
width: calc(200% + 30px);
|
||||
width: calc(200% + #{$card-line-break-width});
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
17
src/presentation/Scripts/Cards/NonCollapsingDirective.ts
Normal file
17
src/presentation/Scripts/Cards/NonCollapsingDirective.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { DirectiveOptions } from 'vue';
|
||||
|
||||
const attributeName = 'data-interactionDoesNotCollapse';
|
||||
|
||||
export function hasDirective(el: Element): boolean {
|
||||
if (el.hasAttribute(attributeName)) {
|
||||
return true;
|
||||
}
|
||||
const parent = el.closest(`[${attributeName}]`);
|
||||
return !!parent;
|
||||
}
|
||||
|
||||
export const NonCollapsing: DirectiveOptions = {
|
||||
inserted(el: HTMLElement) {
|
||||
el.setAttribute(attributeName, '');
|
||||
},
|
||||
};
|
||||
@@ -4,7 +4,7 @@ import { INode } from './SelectableTree/INode';
|
||||
|
||||
export function parseAllCategories(app: IApplication): INode[] | undefined {
|
||||
const nodes = new Array<INode>();
|
||||
for (const category of app.categories) {
|
||||
for (const category of app.actions) {
|
||||
const children = parseCategoryRecursively(category);
|
||||
nodes.push(convertCategoryToNode(category, children));
|
||||
}
|
||||
@@ -23,6 +23,7 @@ export function parseSingleCategory(categoryId: number, app: IApplication): INod
|
||||
export function getScriptNodeId(script: IScript): string {
|
||||
return script.id;
|
||||
}
|
||||
|
||||
export function getCategoryNodeId(category: ICategory): string {
|
||||
return `Category${category.id}`;
|
||||
}
|
||||
@@ -53,6 +54,7 @@ function convertCategoryToNode(
|
||||
text: category.name,
|
||||
children,
|
||||
documentationUrls: category.documentationUrls,
|
||||
isReversible: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -62,5 +64,6 @@ function convertScriptToNode(script: IScript): INode {
|
||||
text: script.name,
|
||||
children: undefined,
|
||||
documentationUrls: script.documentationUrls,
|
||||
isReversible: script.canRevert(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
:selectedNodeIds="selectedNodeIds"
|
||||
:filterPredicate="filterPredicate"
|
||||
:filterText="filterText"
|
||||
v-on:nodeSelected="checkNodeAsync($event)">
|
||||
v-on:nodeSelected="toggleNodeSelectionAsync($event)"
|
||||
v-on:nodeRevertToggled="handleNodeRevertToggleAsync($event)"
|
||||
>
|
||||
</SelectableTree>
|
||||
</span>
|
||||
<span v-else>Nooo 😢</span>
|
||||
@@ -25,6 +27,7 @@
|
||||
import { parseAllCategories, parseSingleCategory, getScriptNodeId, getCategoryNodeId } from './ScriptNodeParser';
|
||||
import SelectableTree, { FilterPredicate } from './SelectableTree/SelectableTree.vue';
|
||||
import { INode } from './SelectableTree/INode';
|
||||
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
@@ -50,13 +53,13 @@
|
||||
await this.initializeNodesAsync(this.categoryId);
|
||||
}
|
||||
|
||||
public async checkNodeAsync(node: INode) {
|
||||
public async toggleNodeSelectionAsync(node: INode) {
|
||||
if (node.children != null && node.children.length > 0) {
|
||||
return; // only interested in script nodes
|
||||
}
|
||||
const state = await this.getCurrentStateAsync();
|
||||
if (!this.selectedNodeIds.some((id) => id === node.id)) {
|
||||
state.selection.addSelectedScript(node.id);
|
||||
state.selection.addSelectedScript(node.id, false);
|
||||
} else {
|
||||
state.selection.removeSelectedScript(node.id);
|
||||
}
|
||||
@@ -71,7 +74,7 @@
|
||||
this.nodes = parseAllCategories(state.app);
|
||||
}
|
||||
this.selectedNodeIds = state.selection.selectedScripts
|
||||
.map((script) => getScriptNodeId(script));
|
||||
.map((selected) => getScriptNodeId(selected.script));
|
||||
}
|
||||
|
||||
public filterPredicate(node: INode): boolean {
|
||||
@@ -81,7 +84,7 @@
|
||||
(category: ICategory) => node.id === getCategoryNodeId(category));
|
||||
}
|
||||
|
||||
private handleSelectionChanged(selectedScripts: ReadonlyArray<IScript>): void {
|
||||
private handleSelectionChanged(selectedScripts: ReadonlyArray<SelectedScript>): void {
|
||||
this.selectedNodeIds = selectedScripts
|
||||
.map((node) => node.id);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<div class="documentationUrls">
|
||||
<a v-for="url of this.documentationUrls"
|
||||
v-bind:key="url"
|
||||
:href="url"
|
||||
:alt="url"
|
||||
target="_blank" class="documentationUrl"
|
||||
v-tooltip.top-center="url"
|
||||
v-on:click.stop>
|
||||
<font-awesome-icon :icon="['fas', 'info-circle']" />
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
/** Wrapper for Liquor Tree, reveals only abstracted INode for communication */
|
||||
@Component
|
||||
export default class DocumentationUrls extends Vue {
|
||||
@Prop() public documentationUrls: string[];
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/presentation/styles/colors.scss";
|
||||
.documentationUrls {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
.documentationUrl {
|
||||
display: flex;
|
||||
color: $gray;
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
&:hover {
|
||||
color: $slate;
|
||||
}
|
||||
&:not(:first-child) {
|
||||
margin-left: 0.1em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,7 @@
|
||||
export interface INode {
|
||||
readonly id: string;
|
||||
readonly text: string;
|
||||
readonly isReversible: boolean;
|
||||
readonly documentationUrls: ReadonlyArray<string>;
|
||||
readonly children?: ReadonlyArray<INode>;
|
||||
}
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
<template>
|
||||
<div id="node">
|
||||
<div>{{ this.data.text }}</div>
|
||||
<div
|
||||
v-for="url of this.data.documentationUrls"
|
||||
v-bind:key="url">
|
||||
<a :href="url"
|
||||
:alt="url"
|
||||
target="_blank" class="docs"
|
||||
v-tooltip.top-center="url"
|
||||
v-on:click.stop>
|
||||
<font-awesome-icon :icon="['fas', 'info-circle']" />
|
||||
</a>
|
||||
</div>
|
||||
<div class="item text">{{ this.data.text }}</div>
|
||||
<RevertToggle
|
||||
class="item"
|
||||
v-if="data.isReversible"
|
||||
:scriptId="data.id" />
|
||||
<DocumentationUrls
|
||||
class="item"
|
||||
v-if="data.documentationUrls && data.documentationUrls.length > 0"
|
||||
:documentationUrls="this.data.documentationUrls" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -19,8 +16,15 @@
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import { INode } from './INode';
|
||||
import RevertToggle from './RevertToggle.vue';
|
||||
import DocumentationUrls from './DocumentationUrls.vue';
|
||||
/** Wrapper for Liquor Tree, reveals only abstracted INode for communication */
|
||||
@Component
|
||||
@Component({
|
||||
components: {
|
||||
RevertToggle,
|
||||
DocumentationUrls,
|
||||
},
|
||||
})
|
||||
export default class Node extends Vue {
|
||||
@Prop() public data: INode;
|
||||
}
|
||||
@@ -30,17 +34,15 @@
|
||||
<style scoped lang="scss">
|
||||
@import "@/presentation/styles/colors.scss";
|
||||
#node {
|
||||
display:flex;
|
||||
flex-direction: row;
|
||||
display:flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
.docs {
|
||||
color: $gray;
|
||||
cursor: pointer;
|
||||
margin-left:5px;
|
||||
&:hover {
|
||||
color: $slate;
|
||||
.text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.item:not(:first-child) {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -12,6 +12,7 @@ export function convertExistingToNode(liquorTreeNode: ILiquorTreeExistingNode):
|
||||
children: (!liquorTreeNode.children || liquorTreeNode.children.length === 0)
|
||||
? [] : liquorTreeNode.children.map((childNode) => convertExistingToNode(childNode)),
|
||||
documentationUrls: liquorTreeNode.data.documentationUrls,
|
||||
isReversible : liquorTreeNode.data.isReversible,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -27,6 +28,7 @@ export function toNewLiquorTreeNode(node: INode): ILiquorTreeNewNode {
|
||||
node.children.map((childNode) => toNewLiquorTreeNode(childNode)),
|
||||
data: {
|
||||
documentationUrls: node.documentationUrls,
|
||||
isReversible: node.isReversible,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
<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>
|
||||
@@ -8,7 +8,7 @@
|
||||
ref="treeElement"
|
||||
>
|
||||
<span class="tree-text" slot-scope="{ node }">
|
||||
<Node :data="convertExistingToNode(node)"/>
|
||||
<Node :data="convertExistingToNode(node)" />
|
||||
</span>
|
||||
</tree>
|
||||
</span>
|
||||
@@ -144,6 +144,7 @@
|
||||
text: oldNode.data.text,
|
||||
data: {
|
||||
documentationUrls: oldNode.data.documentationUrls,
|
||||
isReversible: oldNode.data.isReversible,
|
||||
},
|
||||
children: oldNode.children == null ? [] :
|
||||
updateCheckedState(oldNode.children, selectedNodeIds),
|
||||
@@ -154,9 +155,3 @@
|
||||
return result;
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/presentation/styles/colors.scss";
|
||||
|
||||
</style>
|
||||
@@ -1,14 +1,18 @@
|
||||
<template>
|
||||
<span
|
||||
v-bind:class="{ 'disabled': enabled, 'enabled': !enabled}"
|
||||
v-non-collapsing
|
||||
@click="onClicked()">{{label}}</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue, Emit } from 'vue-property-decorator';
|
||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||
import { NonCollapsing } from '@/presentation/Scripts/Cards/NonCollapsingDirective';
|
||||
|
||||
@Component
|
||||
@Component({
|
||||
directives: { NonCollapsing },
|
||||
})
|
||||
export default class SelectableOption extends StatefulVue {
|
||||
@Prop() public enabled: boolean;
|
||||
@Prop() public label: string;
|
||||
|
||||
@@ -32,7 +32,8 @@ import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||
import SelectableOption from './SelectableOption.vue';
|
||||
import { IApplicationState } from '@/application/State/IApplicationState';
|
||||
import { IScript } from '@/domain/Script';
|
||||
import { IScript } from '@/domain/IScript';
|
||||
import { SelectedScript } from '../../../application/State/Selection/SelectedScript';
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
@@ -79,12 +80,14 @@ export default class TheSelector extends StatefulVue {
|
||||
private updateSelections(state: IApplicationState) {
|
||||
this.isNoneSelected = state.selection.totalSelected === 0;
|
||||
this.isAllSelected = state.selection.totalSelected === state.app.totalScripts;
|
||||
this.isRecommendedSelected = this.areSame(state.app.getRecommendedScripts(), state.selection.selectedScripts);
|
||||
this.isRecommendedSelected = this.areAllRecommended(state.app.getRecommendedScripts(),
|
||||
state.selection.selectedScripts);
|
||||
}
|
||||
|
||||
private areSame(scripts: ReadonlyArray<IScript>, other: ReadonlyArray<IScript>): boolean {
|
||||
private areAllRecommended(scripts: ReadonlyArray<IScript>, other: ReadonlyArray<SelectedScript>): boolean {
|
||||
other = other.filter((selected) => !(selected).revert);
|
||||
return (scripts.length === other.length) &&
|
||||
scripts.every((script) => other.some((s) => s.id === script.id));
|
||||
scripts.every((script) => other.some((selected) => selected.id === script.id));
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="help-container">
|
||||
<TheSelector class="left" />
|
||||
<TheGrouper class="right"
|
||||
<TheSelector />
|
||||
<TheGrouper
|
||||
v-on:groupingChanged="onGroupingChanged($event)"
|
||||
v-show="!this.isSearching" />
|
||||
</div>
|
||||
@@ -119,19 +119,10 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.help-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
.center {
|
||||
justify-content: center;
|
||||
}
|
||||
.left {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.right {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ApplicationState, IApplicationState } from '../application/State/ApplicationState';
|
||||
import { ApplicationState } from '@/application/State/ApplicationState';
|
||||
import { IApplicationState } from '@/application/State/IApplicationState';
|
||||
import { Vue } from 'vue-property-decorator';
|
||||
export { IApplicationState };
|
||||
|
||||
export abstract class StatefulVue extends Vue {
|
||||
public isLoading = true;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Watch, Vue } from 'vue-property-decorator';
|
||||
import { StatefulVue, IApplicationState } from './StatefulVue';
|
||||
import { StatefulVue } from './StatefulVue';
|
||||
import ace from 'ace-builds';
|
||||
import 'ace-builds/webpack-resolver';
|
||||
import { CodeBuilder } from '../application/State/Code/CodeBuilder';
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import { StatefulVue, IApplicationState } from './StatefulVue';
|
||||
import { StatefulVue } from './StatefulVue';
|
||||
import { SaveFileDialog } from './../infrastructure/SaveFileDialog';
|
||||
import { Clipboard } from './../infrastructure/Clipboard';
|
||||
import IconButton from './IconButton.vue';
|
||||
|
||||
@@ -1,20 +1,41 @@
|
||||
<template>
|
||||
<div id="footer">
|
||||
{{text}}
|
||||
<div class="footer">
|
||||
<div class="item">
|
||||
<a :href="releaseUrl" target="_blank">{{ version }}</a>
|
||||
</div>
|
||||
<div class="item">
|
||||
<a @click="$modal.show(modalName)">Privacy</a> <!-- href to #privacy to avoid scrolling to top -->
|
||||
</div>
|
||||
<modal :name="modalName" height="auto" :scrollable="true" :adaptive="true">
|
||||
<div class="modal">
|
||||
<ThePrivacyPolicy class="modal__content"/>
|
||||
<div class="modal__close-button">
|
||||
<font-awesome-icon :icon="['fas', 'times']" @click="$modal.hide(modalName)"/>
|
||||
</div>
|
||||
</div>
|
||||
</modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import { StatefulVue } from './StatefulVue';
|
||||
import ThePrivacyPolicy from './ThePrivacyPolicy.vue';
|
||||
|
||||
@Component
|
||||
@Component({
|
||||
components: {
|
||||
ThePrivacyPolicy,
|
||||
},
|
||||
})
|
||||
export default class TheFooter extends StatefulVue {
|
||||
private text: string = '';
|
||||
private readonly modalName = 'privacy-policy';
|
||||
private version: string = '';
|
||||
private releaseUrl: string = '';
|
||||
|
||||
public async mounted() {
|
||||
const state = await this.getCurrentStateAsync();
|
||||
this.text = `v${state.app.version}`;
|
||||
this.version = `v${state.app.version}`;
|
||||
this.releaseUrl = `${state.app.repositoryUrl}/releases/tag/${state.app.version}`;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -22,10 +43,45 @@ export default class TheFooter extends StatefulVue {
|
||||
<style scoped lang="scss">
|
||||
@import "@/presentation/styles/colors.scss";
|
||||
@import "@/presentation/styles/fonts.scss";
|
||||
#footer {
|
||||
color: $gray;
|
||||
font-size: 0.7em;
|
||||
font-family: $artistic-font;
|
||||
text-align: center;
|
||||
.footer {
|
||||
display: flex;
|
||||
color: $dark-gray;
|
||||
font-size: 1rem;
|
||||
font-family: $normal-font;
|
||||
align-self: center;
|
||||
|
||||
a {
|
||||
color:inherit;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.item:not(:first-child)::before {
|
||||
content: "|";
|
||||
padding: 0 5px;
|
||||
}
|
||||
}
|
||||
.modal {
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
&__content {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__close-button {
|
||||
width: auto;
|
||||
font-size: 1.5em;
|
||||
margin-right:0.25em;
|
||||
align-self: flex-start;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
70
src/presentation/ThePrivacyPolicy.vue
Normal file
70
src/presentation/ThePrivacyPolicy.vue
Normal file
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<div class="privacy-policy">
|
||||
<div class="line">
|
||||
<div class="line__emoji">🚫🍪</div>
|
||||
<div>No cookies!</div>
|
||||
</div>
|
||||
<div class="line">
|
||||
<div class="line__emoji">🚫👀</div>
|
||||
<div>No user behavior / IP adress collection!</div>
|
||||
</div>
|
||||
<div class="line">
|
||||
<div class="line__emoji">🤖</div>
|
||||
<div>Website is deployed automatically from master branch
|
||||
of the <a :href="repositoryUrl" target="_blank">source code</a> with no changes.</div>
|
||||
</div>
|
||||
<div class="line">
|
||||
<div class="line__emoji">📈</div>
|
||||
<div>Basic <a href="https://aws.amazon.com/cloudfront/reporting/" target="_blank">CDN statistics</a>
|
||||
are collected by AWS but they cannot be related to you or your behavior.</div>
|
||||
</div>
|
||||
<div class="line">
|
||||
<div class="line__emoji">🎉</div>
|
||||
<div>As almost no data is colected, the website gets better only with your active feedback.
|
||||
Feel free to <a :href="feedbackUrl" target="_blank">create an issue</a> 😊</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||
|
||||
@Component
|
||||
export default class TheFooter extends StatefulVue {
|
||||
private repositoryUrl: string = '';
|
||||
private feedbackUrl: string = '';
|
||||
|
||||
public async mounted() {
|
||||
const state = await this.getCurrentStateAsync();
|
||||
this.repositoryUrl = state.app.repositoryUrl;
|
||||
this.feedbackUrl = `${state.app.repositoryUrl}/issues`;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/presentation/styles/fonts.scss";
|
||||
.privacy-policy {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-family: $normal-font;
|
||||
text-align:center;
|
||||
|
||||
.line {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-top:0.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color:inherit;
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<div class="search">
|
||||
<input type="search" class="searchTerm" placeholder="Search"
|
||||
<div class="search" v-non-collapsing>
|
||||
<input type="search" class="searchTerm"
|
||||
:placeholder="searchPlaceHolder"
|
||||
@input="updateFilterAsync($event.target.value)" >
|
||||
<div class="iconWrapper">
|
||||
<font-awesome-icon :icon="['fas', 'search']" />
|
||||
@@ -11,9 +12,22 @@
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
|
||||
import { StatefulVue } from './StatefulVue';
|
||||
import { NonCollapsing } from '@/presentation/Scripts/Cards/NonCollapsingDirective';
|
||||
|
||||
@Component
|
||||
@Component( {
|
||||
directives: { NonCollapsing },
|
||||
},
|
||||
)
|
||||
export default class TheSearchBar extends StatefulVue {
|
||||
public searchPlaceHolder = 'Search';
|
||||
|
||||
public async mounted() {
|
||||
const state = await this.getCurrentStateAsync();
|
||||
const totalScripts = state.app.totalScripts;
|
||||
const totalCategories = state.app.totalCategories;
|
||||
this.searchPlaceHolder = `Search in ${totalScripts} scripts`;
|
||||
}
|
||||
|
||||
public async updateFilterAsync(filter: |string) {
|
||||
const state = await this.getCurrentStateAsync();
|
||||
if (!filter) {
|
||||
|
||||
115
tests/unit/application/Parser/ApplicationParser.spec.ts
Normal file
115
tests/unit/application/Parser/ApplicationParser.spec.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { IEntity } from '@/infrastructure/Entity/IEntity';
|
||||
import applicationFile, { YamlCategory, YamlScript, ApplicationYaml } from 'js-yaml-loader!@/application/application.yaml';
|
||||
import { parseApplication } from '@/application/Parser/ApplicationParser';
|
||||
import 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import { parseCategory } from '@/application/Parser/CategoryParser';
|
||||
|
||||
declare var process;
|
||||
|
||||
describe('ApplicationParser', () => {
|
||||
describe('parseApplication', () => {
|
||||
it('can parse current application file', () => {
|
||||
expect(() => parseApplication(applicationFile)).to.not.throw();
|
||||
});
|
||||
it('throws when undefined', () => {
|
||||
expect(() => parseApplication(undefined)).to.throw('application is null or undefined');
|
||||
});
|
||||
it('throws when undefined actions', () => {
|
||||
const sut: ApplicationYaml = {
|
||||
name: 'test',
|
||||
repositoryUrl: 'https://privacy.sexy',
|
||||
actions: undefined,
|
||||
};
|
||||
expect(() => parseApplication(sut)).to.throw('application does not define any action');
|
||||
});
|
||||
it('throws when has no actions', () => {
|
||||
const sut: ApplicationYaml = {
|
||||
name: 'test',
|
||||
repositoryUrl: 'https://privacy.sexy',
|
||||
actions: [],
|
||||
};
|
||||
expect(() => parseApplication(sut)).to.throw('application does not define any action');
|
||||
});
|
||||
it('returns expected name', () => {
|
||||
// arrange
|
||||
const expected = 'test-app-name';
|
||||
const sut: ApplicationYaml = {
|
||||
name: expected,
|
||||
repositoryUrl: 'https://privacy.sexy',
|
||||
actions: [ getTestCategory() ],
|
||||
};
|
||||
// act
|
||||
const actual = parseApplication(sut).name;
|
||||
// assert
|
||||
expect(actual).to.be.equal(actual);
|
||||
});
|
||||
it('returns expected repository url', () => {
|
||||
// arrange
|
||||
const expected = 'https://privacy.sexy';
|
||||
const sut: ApplicationYaml = {
|
||||
name: 'name',
|
||||
repositoryUrl: expected,
|
||||
actions: [ getTestCategory() ],
|
||||
};
|
||||
// act
|
||||
const actual = parseApplication(sut).repositoryUrl;
|
||||
// assert
|
||||
expect(actual).to.be.equal(actual);
|
||||
});
|
||||
it('returns expected repository version', () => {
|
||||
// arrange
|
||||
const expected = '1.0.0';
|
||||
process = {
|
||||
env: {
|
||||
VUE_APP_VERSION: expected,
|
||||
},
|
||||
};
|
||||
const sut: ApplicationYaml = {
|
||||
name: 'name',
|
||||
repositoryUrl: 'https://privacy.sexy',
|
||||
actions: [ getTestCategory() ],
|
||||
};
|
||||
// act
|
||||
const actual = parseApplication(sut).version;
|
||||
// assert
|
||||
expect(actual).to.be.equal(actual);
|
||||
});
|
||||
it('parses actions', () => {
|
||||
// arrange
|
||||
const actions = [ getTestCategory('test1'), getTestCategory('test2') ];
|
||||
const expected = [ parseCategory(actions[0]), parseCategory(actions[1]) ];
|
||||
const sut: ApplicationYaml = {
|
||||
name: 'name',
|
||||
repositoryUrl: 'https://privacy.sexy',
|
||||
actions,
|
||||
};
|
||||
// act
|
||||
const actual = parseApplication(sut).actions;
|
||||
// assert
|
||||
expect(excludingId(actual)).to.be.deep.equal(excludingId(expected));
|
||||
function excludingId<TId>(array: ReadonlyArray<IEntity<TId>>) {
|
||||
return array.map((obj) => {
|
||||
const { ['id']: omitted, ...rest } = obj;
|
||||
return rest;
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function getTestCategory(scriptName = 'testScript'): YamlCategory {
|
||||
return {
|
||||
category: 'category name',
|
||||
children: [ getTestScript(scriptName) ],
|
||||
};
|
||||
}
|
||||
|
||||
function getTestScript(scriptName: string): YamlScript {
|
||||
return {
|
||||
name: scriptName,
|
||||
code: 'script code',
|
||||
revertCode: 'revert code',
|
||||
recommend: true,
|
||||
};
|
||||
}
|
||||
109
tests/unit/application/Parser/CategoryParser.spec.ts
Normal file
109
tests/unit/application/Parser/CategoryParser.spec.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import { parseCategory } from '@/application/Parser/CategoryParser';
|
||||
import { YamlCategory, CategoryOrScript, YamlScript } from 'js-yaml-loader!./application.yaml';
|
||||
import { parseScript } from '@/application/Parser/ScriptParser';
|
||||
import { parseDocUrls } from '@/application/Parser/DocumentationParser';
|
||||
|
||||
describe('CategoryParser', () => {
|
||||
describe('parseCategory', () => {
|
||||
|
||||
it('throws when undefined', () => {
|
||||
expect(() => parseCategory(undefined)).to.throw('category is null or undefined');
|
||||
});
|
||||
|
||||
it('throws when children is empty', () => {
|
||||
const category: YamlCategory = {
|
||||
category: 'test',
|
||||
children: [],
|
||||
};
|
||||
expect(() => parseCategory(category)).to.throw('category has no children');
|
||||
});
|
||||
|
||||
it('throws when children is undefined', () => {
|
||||
const category: YamlCategory = {
|
||||
category: 'test',
|
||||
children: undefined,
|
||||
};
|
||||
expect(() => parseCategory(category)).to.throw('category has no children');
|
||||
});
|
||||
|
||||
it('throws when name is empty', () => {
|
||||
const category: YamlCategory = {
|
||||
category: '',
|
||||
children: getTestChildren(),
|
||||
};
|
||||
expect(() => parseCategory(category)).to.throw('category has no name');
|
||||
});
|
||||
|
||||
it('throws when name is undefined', () => {
|
||||
const category: YamlCategory = {
|
||||
category: undefined,
|
||||
children: getTestChildren(),
|
||||
};
|
||||
expect(() => parseCategory(category)).to.throw('category has no name');
|
||||
});
|
||||
|
||||
it('returns expected docs', () => {
|
||||
// arrange
|
||||
const url = 'https://privacy.sexy';
|
||||
const expected = parseDocUrls({ docs: url });
|
||||
const category: YamlCategory = {
|
||||
category: 'category name',
|
||||
children: getTestChildren(),
|
||||
docs: url,
|
||||
};
|
||||
// act
|
||||
const actual = parseCategory(category).documentationUrls;
|
||||
// assert
|
||||
expect(actual).to.deep.equal(expected);
|
||||
});
|
||||
|
||||
it('returns expected scripts', () => {
|
||||
// arrange
|
||||
const script = getTestScript();
|
||||
const expected = [ parseScript(script) ];
|
||||
const category: YamlCategory = {
|
||||
category: 'category name',
|
||||
children: [ script ],
|
||||
};
|
||||
// act
|
||||
const actual = parseCategory(category).scripts;
|
||||
// assert
|
||||
expect(actual).to.deep.equal(expected);
|
||||
});
|
||||
|
||||
it('returns expected subcategories', () => {
|
||||
// arrange
|
||||
const expected: YamlCategory[] = [ {
|
||||
category: 'test category',
|
||||
children: [ getTestScript() ],
|
||||
}];
|
||||
const category: YamlCategory = {
|
||||
category: 'category name',
|
||||
children: expected,
|
||||
};
|
||||
// act
|
||||
const actual = parseCategory(category).subCategories;
|
||||
// assert
|
||||
expect(actual).to.have.lengthOf(1);
|
||||
expect(actual[0].name).to.equal(expected[0].category);
|
||||
expect(actual[0].scripts.length).to.equal(expected[0].children.length);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function getTestChildren(): ReadonlyArray<CategoryOrScript> {
|
||||
return [
|
||||
getTestScript(),
|
||||
];
|
||||
}
|
||||
|
||||
function getTestScript(): YamlScript {
|
||||
return {
|
||||
name: 'script name',
|
||||
code: 'script code',
|
||||
revertCode: 'revert code',
|
||||
recommend: true,
|
||||
};
|
||||
}
|
||||
39
tests/unit/application/Parser/DocumentationParser.spec.ts
Normal file
39
tests/unit/application/Parser/DocumentationParser.spec.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import { YamlDocumentable } from 'js-yaml-loader!./application.yaml';
|
||||
import { parseDocUrls } from '@/application/Parser/DocumentationParser';
|
||||
|
||||
describe('DocumentationParser', () => {
|
||||
describe('parseDocUrls', () => {
|
||||
it('throws when undefined', () => {
|
||||
expect(() => parseDocUrls(undefined)).to.throw('documentable is null or undefined');
|
||||
});
|
||||
it('returns empty when empty', () => {
|
||||
// arrange
|
||||
const empty: YamlDocumentable = { };
|
||||
// act
|
||||
const actual = parseDocUrls(empty);
|
||||
// assert
|
||||
expect(actual).to.have.lengthOf(0);
|
||||
});
|
||||
it('returns single item when string', () => {
|
||||
// arrange
|
||||
const url = 'https://privacy.sexy';
|
||||
const expected = [ url ];
|
||||
const sut: YamlDocumentable = { docs: url };
|
||||
// act
|
||||
const actual = parseDocUrls(sut);
|
||||
// assert
|
||||
expect(actual).to.deep.equal(expected);
|
||||
});
|
||||
it('returns all when array', () => {
|
||||
// arrange
|
||||
const expected = [ 'https://privacy.sexy', 'https://github.com/undergroundwires/privacy.sexy' ];
|
||||
const sut: YamlDocumentable = { docs: expected };
|
||||
// act
|
||||
const actual = parseDocUrls(sut);
|
||||
// assert
|
||||
expect(actual).to.deep.equal(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
28
tests/unit/application/Parser/ScriptParser.spec.ts
Normal file
28
tests/unit/application/Parser/ScriptParser.spec.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { YamlScript } from 'js-yaml-loader!./application.yaml';
|
||||
import 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import { parseScript } from '@/application/Parser/ScriptParser';
|
||||
import { parseDocUrls } from '@/application/Parser/DocumentationParser';
|
||||
|
||||
describe('ScriptParser', () => {
|
||||
describe('parseScript', () => {
|
||||
it('parseScript parses as expected', () => {
|
||||
// arrange
|
||||
const expected: YamlScript = {
|
||||
name: 'expected name',
|
||||
code: 'expected code',
|
||||
revertCode: 'expected revert code',
|
||||
docs: ['hello.com'],
|
||||
recommend: true,
|
||||
};
|
||||
// act
|
||||
const actual = parseScript(expected);
|
||||
// assert
|
||||
expect(actual.name).to.equal(expected.name);
|
||||
expect(actual.code).to.equal(expected.code);
|
||||
expect(actual.revertCode).to.equal(expected.revertCode);
|
||||
expect(actual.documentationUrls).to.deep.equal(parseDocUrls(expected));
|
||||
expect(actual.isRecommended).to.equal(expected.recommend);
|
||||
});
|
||||
});
|
||||
});
|
||||
65
tests/unit/application/State/Code/ApplicationCode.spec.ts
Normal file
65
tests/unit/application/State/Code/ApplicationCode.spec.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
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';
|
||||
|
||||
describe('ApplicationCode', () => {
|
||||
describe('ctor', () => {
|
||||
it('empty when selection is empty', () => {
|
||||
// arrange
|
||||
const selection = new UserSelection(new ApplicationStub(), []);
|
||||
const sut = new ApplicationCode(selection, 'version');
|
||||
// act
|
||||
const actual = sut.current;
|
||||
// assert
|
||||
expect(actual).to.have.lengthOf(0);
|
||||
});
|
||||
it('has code when selection is not empty', () => {
|
||||
// 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 version = 'version-string';
|
||||
const sut = new ApplicationCode(selection, version);
|
||||
// act
|
||||
const actual = sut.current;
|
||||
// assert
|
||||
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);
|
||||
});
|
||||
it('has code when selection is not 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 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).to.have.length.greaterThan(0).and.include(version);
|
||||
expect(signaled).to.equal(sut.current);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
46
tests/unit/application/State/Filter/FilterResult.spec.ts
Normal file
46
tests/unit/application/State/Filter/FilterResult.spec.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { CategoryStub } from './../../../stubs/CategoryStub';
|
||||
import { ScriptStub } from './../../../stubs/ScriptStub';
|
||||
import { FilterResult } from '@/application/State/Filter/FilterResult';
|
||||
import 'mocha';
|
||||
import { expect } from 'chai';
|
||||
|
||||
describe('FilterResult', () => {
|
||||
describe('hasAnyMatches', () => {
|
||||
it('false when no matches', () => {
|
||||
const sut = new FilterResult(
|
||||
/* scriptMatches */ [],
|
||||
/* categoryMatches */ [],
|
||||
'query',
|
||||
);
|
||||
const actual = sut.hasAnyMatches();
|
||||
expect(actual).to.equal(false);
|
||||
});
|
||||
it('true when script matches', () => {
|
||||
const sut = new FilterResult(
|
||||
/* scriptMatches */ [ new ScriptStub('id') ],
|
||||
/* categoryMatches */ [],
|
||||
'query',
|
||||
);
|
||||
const actual = sut.hasAnyMatches();
|
||||
expect(actual).to.equal(true);
|
||||
});
|
||||
it('true when category matches', () => {
|
||||
const sut = new FilterResult(
|
||||
/* scriptMatches */ [ ],
|
||||
/* categoryMatches */ [ new CategoryStub(5) ],
|
||||
'query',
|
||||
);
|
||||
const actual = sut.hasAnyMatches();
|
||||
expect(actual).to.equal(true);
|
||||
});
|
||||
it('true when script + category matches', () => {
|
||||
const sut = new FilterResult(
|
||||
/* scriptMatches */ [ new ScriptStub('id') ],
|
||||
/* categoryMatches */ [ new CategoryStub(5) ],
|
||||
'query',
|
||||
);
|
||||
const actual = sut.hasAnyMatches();
|
||||
expect(actual).to.equal(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
135
tests/unit/application/State/Filter/UserFilter.spec.ts
Normal file
135
tests/unit/application/State/Filter/UserFilter.spec.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
import { CategoryStub } from './../../../stubs/CategoryStub';
|
||||
import { ScriptStub } from './../../../stubs/ScriptStub';
|
||||
import { IFilterResult } from '@/application/State/Filter/IFilterResult';
|
||||
import { ApplicationStub } from './../../../stubs/ApplicationStub';
|
||||
import { UserFilter } from '@/application/State/Filter/UserFilter';
|
||||
import 'mocha';
|
||||
import { expect } from 'chai';
|
||||
|
||||
describe('UserFilter', () => {
|
||||
it('signals when removing filter', () => {
|
||||
// arrange
|
||||
let isCalled = false;
|
||||
const sut = new UserFilter(new ApplicationStub());
|
||||
sut.filterRemoved.on(() => isCalled = true);
|
||||
// act
|
||||
sut.removeFilter();
|
||||
// assert
|
||||
expect(isCalled).to.be.equal(true);
|
||||
});
|
||||
it('signals when no matches', () => {
|
||||
// arrange
|
||||
let actual: IFilterResult;
|
||||
const nonMatchingFilter = 'non matching filter';
|
||||
const sut = new UserFilter(new ApplicationStub());
|
||||
sut.filtered.on((filterResult) => actual = filterResult);
|
||||
// act
|
||||
sut.setFilter(nonMatchingFilter);
|
||||
// assert
|
||||
expect(actual.hasAnyMatches()).be.equal(false);
|
||||
expect(actual.categoryMatches).to.have.lengthOf(0);
|
||||
expect(actual.scriptMatches).to.have.lengthOf(0);
|
||||
expect(actual.query).to.equal(nonMatchingFilter);
|
||||
});
|
||||
describe('signals when script matches', () => {
|
||||
it('code matches', () => {
|
||||
// arrange
|
||||
const code = 'HELLO world';
|
||||
const filter = 'Hello WoRLD';
|
||||
let actual: IFilterResult;
|
||||
const script = new ScriptStub('id').withCode(code);
|
||||
const category = new CategoryStub(33).withScript(script);
|
||||
const sut = new UserFilter(new ApplicationStub()
|
||||
.withAction(category));
|
||||
sut.filtered.on((filterResult) => actual = filterResult);
|
||||
// act
|
||||
sut.setFilter(filter);
|
||||
// assert
|
||||
expect(actual.hasAnyMatches()).be.equal(true);
|
||||
expect(actual.categoryMatches).to.have.lengthOf(0);
|
||||
expect(actual.scriptMatches).to.have.lengthOf(1);
|
||||
expect(actual.scriptMatches[0]).to.deep.equal(script);
|
||||
expect(actual.query).to.equal(filter);
|
||||
});
|
||||
it('revertCode matches', () => {
|
||||
// arrange
|
||||
const revertCode = 'HELLO world';
|
||||
const filter = 'Hello WoRLD';
|
||||
let actual: IFilterResult;
|
||||
const script = new ScriptStub('id').withRevertCode(revertCode);
|
||||
const category = new CategoryStub(33).withScript(script);
|
||||
const sut = new UserFilter(new ApplicationStub()
|
||||
.withAction(category));
|
||||
sut.filtered.on((filterResult) => actual = filterResult);
|
||||
// act
|
||||
sut.setFilter(filter);
|
||||
// assert
|
||||
expect(actual.hasAnyMatches()).be.equal(true);
|
||||
expect(actual.categoryMatches).to.have.lengthOf(0);
|
||||
expect(actual.scriptMatches).to.have.lengthOf(1);
|
||||
expect(actual.scriptMatches[0]).to.deep.equal(script);
|
||||
expect(actual.query).to.equal(filter);
|
||||
});
|
||||
it('name matches', () => {
|
||||
// arrange
|
||||
const name = 'HELLO world';
|
||||
const filter = 'Hello WoRLD';
|
||||
let actual: IFilterResult;
|
||||
const script = new ScriptStub('id').withName(name);
|
||||
const category = new CategoryStub(33).withScript(script);
|
||||
const sut = new UserFilter(new ApplicationStub()
|
||||
.withAction(category));
|
||||
sut.filtered.on((filterResult) => actual = filterResult);
|
||||
// act
|
||||
sut.setFilter(filter);
|
||||
// assert
|
||||
expect(actual.hasAnyMatches()).be.equal(true);
|
||||
expect(actual.categoryMatches).to.have.lengthOf(0);
|
||||
expect(actual.scriptMatches).to.have.lengthOf(1);
|
||||
expect(actual.scriptMatches[0]).to.deep.equal(script);
|
||||
expect(actual.query).to.equal(filter);
|
||||
});
|
||||
});
|
||||
it('signals when category matches', () => {
|
||||
// arrange
|
||||
const categoryName = 'HELLO world';
|
||||
const filter = 'Hello WoRLD';
|
||||
let actual: IFilterResult;
|
||||
const category = new CategoryStub(55).withName(categoryName);
|
||||
const sut = new UserFilter(new ApplicationStub()
|
||||
.withAction(category));
|
||||
sut.filtered.on((filterResult) => actual = filterResult);
|
||||
// act
|
||||
sut.setFilter(filter);
|
||||
// assert
|
||||
expect(actual.hasAnyMatches()).be.equal(true);
|
||||
expect(actual.categoryMatches).to.have.lengthOf(1);
|
||||
expect(actual.categoryMatches[0]).to.deep.equal(category);
|
||||
expect(actual.scriptMatches).to.have.lengthOf(0);
|
||||
expect(actual.query).to.equal(filter);
|
||||
});
|
||||
it('signals when category and script matches', () => {
|
||||
// arrange
|
||||
const matchingText = 'HELLO world';
|
||||
const filter = 'Hello WoRLD';
|
||||
let actual: IFilterResult;
|
||||
const script = new ScriptStub('script')
|
||||
.withName(matchingText);
|
||||
const category = new CategoryStub(55)
|
||||
.withName(matchingText)
|
||||
.withScript(script);
|
||||
const app = new ApplicationStub()
|
||||
.withAction(category);
|
||||
const sut = new UserFilter(app);
|
||||
sut.filtered.on((filterResult) => actual = filterResult);
|
||||
// act
|
||||
sut.setFilter(filter);
|
||||
// assert
|
||||
expect(actual.hasAnyMatches()).be.equal(true);
|
||||
expect(actual.categoryMatches).to.have.lengthOf(1);
|
||||
expect(actual.categoryMatches[0]).to.deep.equal(category);
|
||||
expect(actual.scriptMatches).to.have.lengthOf(1);
|
||||
expect(actual.scriptMatches[0]).to.deep.equal(script);
|
||||
expect(actual.query).to.equal(filter);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import { ScriptStub } from './../../../stubs/ScriptStub';
|
||||
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
||||
import 'mocha';
|
||||
import { expect } from 'chai';
|
||||
|
||||
describe('SelectedScript', () => {
|
||||
it('id is same as script id', () => {
|
||||
// arrange
|
||||
const expectedId = 'scriptId';
|
||||
const script = new ScriptStub(expectedId);
|
||||
const sut = new SelectedScript(script, false);
|
||||
// act
|
||||
const actualId = sut.id;
|
||||
// assert
|
||||
expect(actualId).to.equal(expectedId);
|
||||
});
|
||||
it('throws when revert is true for irreversible script', () => {
|
||||
// arrange
|
||||
const expectedId = 'scriptId';
|
||||
const script = new ScriptStub(expectedId)
|
||||
.withRevertCode(undefined);
|
||||
// act
|
||||
function construct() { new SelectedScript(script, true); } // tslint:disable-line:no-unused-expression
|
||||
// assert
|
||||
expect(construct).to.throw('cannot revert an irreversible script');
|
||||
});
|
||||
|
||||
});
|
||||
96
tests/unit/application/State/Selection/UserSelection.spec.ts
Normal file
96
tests/unit/application/State/Selection/UserSelection.spec.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
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', () => {
|
||||
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 sut = new UserSelection(app, selectedScripts);
|
||||
sut.changed.on((newScripts) => events.push(newScripts));
|
||||
// act
|
||||
sut.deselectAll();
|
||||
// assert
|
||||
expect(sut.selectedScripts).to.have.length(0);
|
||||
expect(events).to.have.lengthOf(1);
|
||||
expect(events[0]).to.have.length(0);
|
||||
});
|
||||
it('selectOnly selects expected', () => {
|
||||
// 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 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));
|
||||
// act
|
||||
sut.selectOnly(scripts);
|
||||
// assert
|
||||
expect(sut.selectedScripts).to.deep.equal(expected);
|
||||
expect(events).to.have.lengthOf(1);
|
||||
expect(events[0]).to.deep.equal(expected);
|
||||
});
|
||||
it('selectAll selects as expected', () => {
|
||||
// arrange
|
||||
const events: Array<readonly SelectedScript[]> = [];
|
||||
const scripts: IScript[] = [new ScriptStub('s1'), new ScriptStub('s2'), new ScriptStub('s3'), new ScriptStub('s4')];
|
||||
const app = new ApplicationStub()
|
||||
.withAction(new CategoryStub(1)
|
||||
.withScripts(...scripts));
|
||||
const sut = new UserSelection(app, []);
|
||||
sut.changed.on((newScripts) => events.push(newScripts));
|
||||
const expected = scripts.map((script) => new SelectedScript(script, false));
|
||||
// act
|
||||
sut.selectAll();
|
||||
// assert
|
||||
expect(sut.selectedScripts).to.deep.equal(expected);
|
||||
expect(events).to.have.lengthOf(1);
|
||||
expect(events[0]).to.deep.equal(expected);
|
||||
});
|
||||
describe('addOrUpdateSelectedScript', () => {
|
||||
it('adds when item does not exist', () => {
|
||||
// arrange
|
||||
const events: Array<readonly SelectedScript[]> = [];
|
||||
const app = new ApplicationStub()
|
||||
.withAction(new CategoryStub(1)
|
||||
.withScripts(new ScriptStub('s1'), new ScriptStub('s2')));
|
||||
const sut = new UserSelection(app, []);
|
||||
sut.changed.on((scripts) => events.push(scripts));
|
||||
const expected = [ new SelectedScript(new ScriptStub('s1'), false) ];
|
||||
// act
|
||||
sut.addOrUpdateSelectedScript('s1', false);
|
||||
// assert
|
||||
expect(sut.selectedScripts).to.deep.equal(expected);
|
||||
expect(events).to.have.lengthOf(1);
|
||||
expect(events[0]).to.deep.equal(expected);
|
||||
});
|
||||
it('updates when item exists', () => {
|
||||
// arrange
|
||||
const events: Array<readonly SelectedScript[]> = [];
|
||||
const app = new ApplicationStub()
|
||||
.withAction(new CategoryStub(1)
|
||||
.withScripts(new ScriptStub('s1'), new ScriptStub('s2')));
|
||||
const sut = new UserSelection(app, []);
|
||||
sut.changed.on((scripts) => events.push(scripts));
|
||||
const expected = [ new SelectedScript(new ScriptStub('s1'), true) ];
|
||||
// act
|
||||
sut.addOrUpdateSelectedScript('s1', true);
|
||||
// assert
|
||||
expect(sut.selectedScripts).to.deep.equal(expected);
|
||||
expect(events).to.have.lengthOf(1);
|
||||
expect(events[0]).to.deep.equal(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,41 +0,0 @@
|
||||
import { CategoryStub } from './../stubs/CategoryStub';
|
||||
import { ApplicationStub } from './../stubs/ApplicationStub';
|
||||
import { ScriptStub } from './../stubs/ScriptStub';
|
||||
import { UserSelection } from '@/application/State/Selection/UserSelection';
|
||||
import 'mocha';
|
||||
import { expect } from 'chai';
|
||||
|
||||
|
||||
describe('UserSelection', () => {
|
||||
it('deselectAll removes all items', async () => {
|
||||
// arrange
|
||||
const app = new ApplicationStub()
|
||||
.withCategory(new CategoryStub(1)
|
||||
.withScriptIds('s1', 's2', 's3', 's4'));
|
||||
const selectedScripts = [new ScriptStub('s1'), new ScriptStub('s2'), new ScriptStub('s3')];
|
||||
const sut = new UserSelection(app, selectedScripts);
|
||||
|
||||
// act
|
||||
sut.deselectAll();
|
||||
const actual = sut.selectedScripts;
|
||||
|
||||
// assert
|
||||
expect(actual, JSON.stringify(sut.selectedScripts)).to.have.length(0);
|
||||
});
|
||||
it('selectOnly selects expected', async () => {
|
||||
// arrange
|
||||
const app = new ApplicationStub()
|
||||
.withCategory(new CategoryStub(1)
|
||||
.withScriptIds('s1', 's2', 's3', 's4'));
|
||||
const selectedScripts = [new ScriptStub('s1'), new ScriptStub('s2'), new ScriptStub('s3')];
|
||||
const sut = new UserSelection(app, selectedScripts);
|
||||
const expected = [new ScriptStub('s2'), new ScriptStub('s3'), new ScriptStub('s4')];
|
||||
|
||||
// act
|
||||
sut.selectOnly(expected);
|
||||
const actual = sut.selectedScripts;
|
||||
|
||||
// assert
|
||||
expect(actual).to.deep.equal(expected);
|
||||
});
|
||||
});
|
||||
@@ -59,4 +59,28 @@ describe('Application', () => {
|
||||
// assert
|
||||
expect(construct).to.throw('Application must consist of at least one recommended script');
|
||||
});
|
||||
it('totalScripts counts right', () => {
|
||||
// arrange
|
||||
const categories = [
|
||||
new CategoryStub(1).withScripts(new ScriptStub('S1').withIsRecommended(true)),
|
||||
new CategoryStub(2).withScripts(new ScriptStub('S2'), new ScriptStub('S3')),
|
||||
new CategoryStub(3).withCategories(new CategoryStub(4).withScripts(new ScriptStub('S4'))),
|
||||
];
|
||||
// act
|
||||
const application = new Application('name', 'repo', '0.1.0', categories);
|
||||
// assert
|
||||
expect(application.totalScripts).to.equal(4);
|
||||
});
|
||||
it('totalCategories counts right', () => {
|
||||
// arrange
|
||||
const categories = [
|
||||
new CategoryStub(1).withScripts(new ScriptStub('S1').withIsRecommended(true)),
|
||||
new CategoryStub(2).withScripts(new ScriptStub('S2'), new ScriptStub('S3')),
|
||||
new CategoryStub(3).withCategories(new CategoryStub(4).withScripts(new ScriptStub('S4'))),
|
||||
];
|
||||
// act
|
||||
const application = new Application('name', 'repo', '0.1.0', categories);
|
||||
// assert
|
||||
expect(application.totalCategories).to.equal(4);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,15 +3,44 @@ import { expect } from 'chai';
|
||||
import { Script } from '@/domain/Script';
|
||||
|
||||
describe('Script', () => {
|
||||
|
||||
it('cannot construct with duplicate lines', () => {
|
||||
// arrange
|
||||
const code = 'duplicate\nduplicate\ntest\nduplicate';
|
||||
|
||||
// act
|
||||
function construct() { return new Script('ScriptName', code, [], true); }
|
||||
|
||||
// assert
|
||||
expect(construct).to.throw();
|
||||
describe('ctor', () => {
|
||||
describe('code', () => {
|
||||
it('cannot construct with duplicate lines', () => {
|
||||
const code = 'duplicate\nduplicate\ntest\nduplicate';
|
||||
expect(() => createWithCode(code)).to.throw();
|
||||
});
|
||||
it('cannot construct with empty lines', () => {
|
||||
const code = 'duplicate\n\n\ntest\nduplicate';
|
||||
expect(() => createWithCode(code)).to.throw();
|
||||
});
|
||||
});
|
||||
describe('revertCode', () => {
|
||||
it('cannot construct with duplicate lines', () => {
|
||||
const code = 'duplicate\nduplicate\ntest\nduplicate';
|
||||
expect(() => createWithCode('REM', code)).to.throw();
|
||||
});
|
||||
it('cannot construct with empty lines', () => {
|
||||
const code = 'duplicate\n\n\ntest\nduplicate';
|
||||
expect(() => createWithCode('REM', code)).to.throw();
|
||||
});
|
||||
it('cannot construct with when same as code', () => {
|
||||
const code = 'REM';
|
||||
expect(() => createWithCode(code, code)).to.throw();
|
||||
});
|
||||
});
|
||||
describe('canRevert', () => {
|
||||
it('returns false without revert code', () => {
|
||||
const sut = createWithCode('code');
|
||||
expect(sut.canRevert()).to.equal(false);
|
||||
});
|
||||
it('returns true with revert code', () => {
|
||||
const sut = createWithCode('code', 'non empty revert code');
|
||||
expect(sut.canRevert()).to.equal(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function createWithCode(code: string, revertCode?: string): Script {
|
||||
return new Script('name', code, revertCode, [], false);
|
||||
}
|
||||
|
||||
@@ -8,15 +8,15 @@ describe('InMemoryRepository', () => {
|
||||
[new NumericEntityStub(1), new NumericEntityStub(2), new NumericEntityStub(3)]);
|
||||
|
||||
describe('item exists', () => {
|
||||
const actual = sut.exists(new NumericEntityStub(1));
|
||||
const actual = sut.exists(1);
|
||||
it('returns true', () => expect(actual).to.be.true);
|
||||
});
|
||||
describe('item does not exist', () => {
|
||||
const actual = sut.exists(new NumericEntityStub(99));
|
||||
const actual = sut.exists(99);
|
||||
it('returns false', () => expect(actual).to.be.false);
|
||||
});
|
||||
});
|
||||
it('can get', () => {
|
||||
it('getItems gets initial items', () => {
|
||||
// arrange
|
||||
const expected = [
|
||||
new NumericEntityStub(1), new NumericEntityStub(2), new NumericEntityStub(3), new NumericEntityStub(4)];
|
||||
@@ -28,7 +28,7 @@ describe('InMemoryRepository', () => {
|
||||
// assert
|
||||
expect(actual).to.deep.equal(expected);
|
||||
});
|
||||
it('can add', () => {
|
||||
it('addItem adds', () => {
|
||||
// arrange
|
||||
const sut = new InMemoryRepository<number, NumericEntityStub>();
|
||||
const expected = {
|
||||
@@ -47,7 +47,7 @@ describe('InMemoryRepository', () => {
|
||||
expect(actual.length).to.equal(expected.length);
|
||||
expect(actual.item).to.deep.equal(expected.item);
|
||||
});
|
||||
it('can remove', () => {
|
||||
it('removeItem removes', () => {
|
||||
// arrange
|
||||
const initialItems = [
|
||||
new NumericEntityStub(1), new NumericEntityStub(2), new NumericEntityStub(3), new NumericEntityStub(4)];
|
||||
@@ -69,4 +69,30 @@ describe('InMemoryRepository', () => {
|
||||
expect(actual.length).to.equal(expected.length);
|
||||
expect(actual.items).to.deep.equal(expected.items);
|
||||
});
|
||||
describe('addOrUpdateItem', () => {
|
||||
it('adds when item does not exist', () => {
|
||||
// arrange
|
||||
const initialItems = [ new NumericEntityStub(1), new NumericEntityStub(2) ];
|
||||
const newItem = new NumericEntityStub(3);
|
||||
const expected = [ ...initialItems, newItem ];
|
||||
const sut = new InMemoryRepository<number, NumericEntityStub>(initialItems);
|
||||
// act
|
||||
sut.addOrUpdateItem(newItem);
|
||||
// assert
|
||||
const actual = sut.getItems();
|
||||
expect(actual).to.deep.equal(expected);
|
||||
});
|
||||
it('updates when item exists', () => {
|
||||
// arrange
|
||||
const initialItems = [ new NumericEntityStub(1).withCustomProperty('bca') ];
|
||||
const updatedItem = new NumericEntityStub(1).withCustomProperty('abc');
|
||||
const expected = [ updatedItem ];
|
||||
const sut = new InMemoryRepository<number, NumericEntityStub>(initialItems);
|
||||
// act
|
||||
sut.addOrUpdateItem(updatedItem);
|
||||
// assert
|
||||
const actual = sut.getItems();
|
||||
expect(actual).to.deep.equal(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -35,16 +35,20 @@ describe('Signal Tests', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
receivers = [
|
||||
new ReceiverMock(), new ReceiverMock(),
|
||||
new ReceiverMock(), new ReceiverMock()];
|
||||
for (const receiver of receivers) {
|
||||
new ReceiverMock(), new ReceiverMock(),
|
||||
new ReceiverMock(), new ReceiverMock()];
|
||||
function subscribeReceiver(receiver: ReceiverMock) {
|
||||
signal.on((arg) => receiver.onReceive(arg));
|
||||
}});
|
||||
}
|
||||
for (const receiver of receivers) {
|
||||
subscribeReceiver(receiver);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
it('notify() should execute all callbacks', () => {
|
||||
signal.notify(5);
|
||||
receivers.every((receiver) => {
|
||||
receivers.forEach((receiver) => {
|
||||
expect(receiver.onRecieveCalls).to.have.length(1);
|
||||
});
|
||||
});
|
||||
@@ -52,7 +56,7 @@ describe('Signal Tests', () => {
|
||||
it('notify() should execute all callbacks with payload', () => {
|
||||
const expected = 5;
|
||||
signal.notify(expected);
|
||||
receivers.every((receiver) => {
|
||||
receivers.forEach((receiver) => {
|
||||
expect(receiver.onRecieveCalls).to.deep.equal([expected]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { IApplication, ICategory, IScript } from '@/domain/IApplication';
|
||||
|
||||
export class ApplicationStub implements IApplication {
|
||||
public readonly totalScripts = 0;
|
||||
public readonly totalCategories = 0;
|
||||
public totalScripts = 0;
|
||||
public totalCategories = 0;
|
||||
public readonly name = 'StubApplication';
|
||||
public readonly repositoryUrl = 'https://privacy.sexy';
|
||||
public readonly version = '0.1.0';
|
||||
public readonly categories = new Array<ICategory>();
|
||||
public readonly actions = new Array<ICategory>();
|
||||
|
||||
public withCategory(category: ICategory): IApplication {
|
||||
this.categories.push(category);
|
||||
public withAction(category: ICategory): IApplication {
|
||||
this.actions.push(category);
|
||||
return this;
|
||||
}
|
||||
public findCategory(categoryId: number): ICategory {
|
||||
@@ -19,12 +19,51 @@ export class ApplicationStub implements IApplication {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public findScript(scriptId: string): IScript {
|
||||
throw new Error('Method not implemented.');
|
||||
return this.getAllScripts().find((script) => scriptId === script.id);
|
||||
}
|
||||
public getAllScripts(): ReadonlyArray<IScript> {
|
||||
throw new Error('Method not implemented.');
|
||||
const scripts = [];
|
||||
for (const category of this.actions) {
|
||||
const categoryScripts = getScriptsRecursively(category);
|
||||
scripts.push(...categoryScripts);
|
||||
}
|
||||
return scripts;
|
||||
}
|
||||
public getAllCategories(): ReadonlyArray<ICategory> {
|
||||
throw new Error('Method not implemented.');
|
||||
const categories = [];
|
||||
categories.push(...this.actions);
|
||||
for (const category of this.actions) {
|
||||
const subCategories = getSubCategoriesRecursively(category);
|
||||
categories.push(...subCategories);
|
||||
}
|
||||
return categories;
|
||||
}
|
||||
}
|
||||
|
||||
function getSubCategoriesRecursively(category: ICategory): ReadonlyArray<ICategory> {
|
||||
const subCategories = [];
|
||||
if (category.subCategories) {
|
||||
for (const subCategory of category.subCategories) {
|
||||
subCategories.push(subCategory);
|
||||
subCategories.push(...getSubCategoriesRecursively(subCategory));
|
||||
}
|
||||
}
|
||||
return subCategories;
|
||||
}
|
||||
|
||||
|
||||
function getScriptsRecursively(category: ICategory): ReadonlyArray<IScript> {
|
||||
const categoryScripts = [];
|
||||
if (category.scripts) {
|
||||
for (const script of category.scripts) {
|
||||
categoryScripts.push(script);
|
||||
}
|
||||
}
|
||||
if (category.subCategories) {
|
||||
for (const subCategory of category.subCategories) {
|
||||
const subCategoryScripts = getScriptsRecursively(subCategory);
|
||||
categoryScripts.push(...subCategoryScripts);
|
||||
}
|
||||
}
|
||||
return categoryScripts;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { ScriptStub } from './ScriptStub';
|
||||
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
|
||||
import { ICategory, IScript } from '@/domain/ICategory';
|
||||
import { ScriptStub } from './ScriptStub';
|
||||
|
||||
export class CategoryStub extends BaseEntity<number> implements ICategory {
|
||||
public readonly name = `category-with-id-${this.id}`;
|
||||
public name = `category-with-id-${this.id}`;
|
||||
public readonly subCategories = new Array<ICategory>();
|
||||
public readonly scripts = new Array<IScript>();
|
||||
public readonly documentationUrls = new Array<string>();
|
||||
@@ -13,14 +13,32 @@ export class CategoryStub extends BaseEntity<number> implements ICategory {
|
||||
}
|
||||
public withScriptIds(...scriptIds: string[]): CategoryStub {
|
||||
for (const scriptId of scriptIds) {
|
||||
this.scripts.push(new ScriptStub(scriptId));
|
||||
this.withScript(new ScriptStub(scriptId));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
public withScripts(...scripts: IScript[]): CategoryStub {
|
||||
for (const script of scripts) {
|
||||
this.scripts.push(script);
|
||||
this.withScript(script);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
public withCategories(...categories: ICategory[]): CategoryStub {
|
||||
for (const category of categories) {
|
||||
this.withCategory(category);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
public withCategory(category: ICategory): CategoryStub {
|
||||
this.subCategories.push(category);
|
||||
return this;
|
||||
}
|
||||
public withScript(script: IScript): CategoryStub {
|
||||
this.scripts.push(script);
|
||||
return this;
|
||||
}
|
||||
public withName(categoryName: string) {
|
||||
this.name = categoryName;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
|
||||
|
||||
export class NumericEntityStub extends BaseEntity<number> {
|
||||
public customProperty = 'customProperty';
|
||||
constructor(id: number) {
|
||||
super(id);
|
||||
}
|
||||
public withCustomProperty(value: string): NumericEntityStub {
|
||||
this.customProperty = value;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,34 @@
|
||||
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
|
||||
import { IScript } from './../../../src/domain/IScript';
|
||||
import { IScript } from '@/domain/IScript';
|
||||
|
||||
export class ScriptStub extends BaseEntity<string> implements IScript {
|
||||
public readonly name = `name${this.id}`;
|
||||
public readonly code = `name${this.id}`;
|
||||
public name = `name${this.id}`;
|
||||
public code = `REM code${this.id}`;
|
||||
public revertCode = `REM revertCode${this.id}`;
|
||||
public readonly documentationUrls = new Array<string>();
|
||||
public isRecommended = false;
|
||||
public isRecommended = true;
|
||||
|
||||
constructor(public readonly id: string) {
|
||||
super(id);
|
||||
}
|
||||
public canRevert(): boolean {
|
||||
return Boolean(this.revertCode);
|
||||
}
|
||||
|
||||
public withIsRecommended(value: boolean): ScriptStub {
|
||||
this.isRecommended = value;
|
||||
return this;
|
||||
}
|
||||
public withCode(value: string): ScriptStub {
|
||||
this.code = value;
|
||||
return this;
|
||||
}
|
||||
public withName(name: string): ScriptStub {
|
||||
this.name = name;
|
||||
return this;
|
||||
}
|
||||
public withRevertCode(revertCode: string): ScriptStub {
|
||||
this.revertCode = revertCode;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user