├── .eslintrc.cjs ├── .github └── workflows │ └── gh-pages.yml ├── .gitignore ├── .prettierrc ├── .vscode └── extensions.json ├── CHANGELOG.md ├── README.md ├── env.d.ts ├── index.html ├── package-lock.json ├── package.json ├── public └── favicon.ico ├── src ├── App.vue ├── assets │ ├── header.svg │ ├── main.css │ ├── mixins.scss │ └── questions.json ├── components │ ├── answeroption-extras │ │ ├── answer-extra-hint.vue │ │ └── answer-extra-input.vue │ ├── dqb-answeroption-extras.vue │ ├── dqb-answeroption.vue │ ├── dqb-dataviewQuery.vue │ ├── dqb-footer.vue │ ├── dqb-header.vue │ ├── dqb-navigation.vue │ ├── dqb-question.vue │ └── dqb-routerButton.vue ├── interfaces │ └── question.ts ├── main.ts ├── router │ └── index.ts ├── stores │ └── questions.store.ts ├── utilities │ ├── conditionString.utility.spec.ts │ ├── conditionString.utility.ts │ └── dataviewQuery.utility.ts └── views │ ├── PrivacyView.vue │ ├── QuestionView.vue │ ├── ResultView.vue │ └── StartView.vue ├── tsconfig.app.json ├── tsconfig.config.json ├── tsconfig.json ├── tsconfig.vitest.json └── vite.config.ts /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | require("@rushstack/eslint-patch/modern-module-resolution"); 3 | 4 | module.exports = { 5 | root: true, 6 | extends: [ 7 | "plugin:vue/vue3-essential", 8 | "eslint:recommended", 9 | "@vue/eslint-config-typescript", 10 | "@vue/eslint-config-prettier", 11 | ], 12 | parserOptions: { 13 | ecmaVersion: "latest", 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Github Pages 2 | on: 3 | push: 4 | branches: [ main ] 5 | 6 | jobs: 7 | build-and-deploy: 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@master 12 | 13 | - name: Create Node Environment 14 | uses: actions/setup-node@v1 15 | with: 16 | node-version: 18.x 17 | 18 | - name: Install and build project 19 | run: | 20 | npm install 21 | npm run build-only 22 | - name: Deploy 23 | uses: s0/git-publish-subdir-action@develop 24 | env: 25 | REPO: self 26 | BRANCH: gh-pages 27 | FOLDER: dist 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist-ssr 14 | coverage 15 | *.local 16 | 17 | /cypress/videos/ 18 | /cypress/screenshots/ 19 | 20 | # Editor directories and files 21 | .vscode/* 22 | !.vscode/extensions.json 23 | .idea 24 | *.suo 25 | *.ntvs* 26 | *.njsproj 27 | *.sln 28 | *.sw? 29 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "endOfLine": "auto" 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"] 3 | } 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.2.5 4 | 5 | - Update dependencies 6 | 7 | ## 1.2.4 8 | 9 | - Update dependencies 10 | - Fix recursive change detections that got introduced while updating dependencies 11 | - Fix Links to documentation 12 | 13 | ## 1.2.3 14 | 15 | - Update dependencies 16 | 17 | ## v1.2.2 18 | 19 | - Removed duplicated answeroption from "How do you want the results sorted?" 20 | - Update dependencies 21 | 22 | ## v1.2.1 23 | 24 | - Fix #21 - answer option extras weren't loaded on first answer option 25 | - Enhance wording of questions and app (Thanks Denise!) 26 | 27 | ## v1.2.0 28 | 29 | - Add explanation about question.json to README 30 | - Duplicate navigation buttons after the question for easier navigation 31 | - Add conditional questions that are only shown when the query fulfills a condition 32 | - Add appendix functionality that add dataview to an existing row instead of creating a new one 33 | 34 | ## v1.1.0 35 | 36 | - Disable previous button on first question 37 | - Disable next button when no answer is selected 38 | - Add highlighting to the currently "editable" row of the dataview query 39 | - Small adjustments and added info blocks for several questions 40 | 41 | ## v1.0.1 42 | 43 | - Fix navigation alignment on mobile resolutions 44 | - Add link to Changelog to footer 45 | 46 | ## v1.0.0 47 | 48 | Initial release with 6 Questions. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Basic Dataview Query builder 2 | 3 | ## 🧱 [Click here to access the Basic Query Builder!](https://s-blu.github.io/basic-dataview-query-builder/) 🧱 4 | 5 | ## What is this? 6 | 7 | The "Basic Dataview Query Builder" is a webpage that allows you the creation of basic dataview queries. [Dataview](https://blacksmithgu.github.io/obsidian-dataview/) is a popular community plugin for the Personal Knowledge Management software [Obsidian.md](https://obsidian.md/), both brilliant and highly flexible applications that can be a bit difficult to understand in the beginning. 8 | 9 | To make your start into dataview a tad more easier, the Basic Dataview Query Builder allows you to click together your first queries that'll help you understand the syntax of the plugin and guide you through the different pieces of informations needed to build a useable query. 10 | 11 | We hope you enjoy! 12 | 13 | If you find a bug, have a question or would like to see another feature, feel free to open a issue at the [github repository](https://github.com/s-blu/basic-dataview-query-builder). 14 | 15 | ## Development/Contribution 16 | 17 | **If you want to use the basic dataview query builder, click the link at the very top of this information!** The following information is for developing and customizing the basic dataview query. 18 | 19 | This page is based on a Vue 3 application built with Vite, styled with [Bulma.io](https://bulma.io). 20 | 21 | ### Questions JSON 22 | 23 | The questions of the Basic Query Builder are stored in a .json for easy maintainability and exchangebility. That makes it theoretically possible to build multiple query builders, i.e. some that focus on a specific part of dataview. To be recognized by the query builder, the JSON file needs to follow a certain structure. 24 | 25 | The JSON contains an **Array** with **Question objects**. 26 | 27 | #### Object structures 28 | 29 | **Question** 30 | 31 | ``` 32 | question: string // The question itself 33 | subtitle: string // Subtitle or Category of the question, shown in the navigation (desktop only) 34 | answers: AnswerOption[] // array of all available answers 35 | condition: ConditionString // (optional) If present, determines based on the available queryParts if the question should be asked or not 36 | appendix: IndexString // (optional) If present, will add the dataview of this questions answer to the existing one specified here 37 | ``` 38 | 39 | **Condition String** 40 | 41 | A condition string is a normal string that needs to follow a convention. It checks if the given words are part of the query (case sensitive) and helps decide if the conditioned question is relevant to ask or not. The condition is always executed on the raw dataview, without variable (user input) replacement. 42 | 43 | - `WORD` Mandatory Word. Needs to be present. 44 | - `-WORD` Forbidden word. Cannot be present. 45 | - `~WORD` OR. Needs to be combined with mandatory or forbidden words to create OR concantinations 46 | 47 | _Examples_ 48 | 49 | - `TABLE` Question will be shown if TABLE is present in current query (if it is a TABLE query) 50 | - `WHERE~FROM` Question will be shown if either WHERE or FROM or both are present 51 | - `TABLE -GROUP` Question will be shown if TABLE is present and GROUP is not. 52 | - `TABLE FLATTEN -GROUP~LIMIT` Question will be shown if TABLE and FLATTEN is available, but GROUP or LIMIT are not. 53 | - `LIST~TABLE~TASK FLATTEN -GROUP` Question will be shown if FLATTEN and LIST or TABLE or TASK are available and GROUP is not. 54 | 55 | **Index String** 56 | 57 | Gives either a absolute or a relative index. To mark a index as relative to the current question, add a `.` in front. 58 | 59 | _Examples_ 60 | 61 | - `2` refers to the third question (array index 2) in the questions array 62 | - `0` refers to the first question 63 | - `-1` refers to the last question 64 | - `.-1` refers to the question right before this question 65 | - `.2` refers to the second question that comes after this question 66 | 67 | **Answeroptions** 68 | 69 | ``` 70 | label: string // the text of the answer 71 | dataview: string // dataview the answer produces. Put variables that should be replaceable in {{}} 72 | readmore: string // (optional) link to the official dataview documentation that gives more info about the resulting dataview 73 | extras: AnswerOptionExtras[] // (optional) additional UI elements associated with this answer 74 | ``` 75 | 76 | **AnswerOptionExtras: Input** 77 | 78 | Renders a text input field that can be used to replace placeholders in the answeroptions dataview. 79 | 80 | ``` 81 | type: "input" // type of extra. 82 | label: string // label of the input field 83 | variabletype: string // (optional) type of variable to perform specific operations on input. Currently available: metadata 84 | varname: string // variable name used in the dataview string of the answer that should be replaced with this input value 85 | ``` 86 | 87 | **AnswerOptionExtras: Hint** 88 | 89 | Displays a message box/notification with some custom text that can give hints or additional context. 90 | 91 | ``` 92 | type: "hint" // type of extra. 93 | title: string // (optional) Title of the message box 94 | message: string // message of the message box 95 | ``` 96 | 97 | ### Recommended IDE Setup 98 | 99 | [VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin). 100 | 101 | ### Type Support for `.vue` Imports in TS 102 | 103 | TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types. 104 | 105 | If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps: 106 | 107 | 1. Disable the built-in TypeScript Extension 108 | 1. Run `Extensions: Show Built-in Extensions` from VSCode's command palette 109 | 2. Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)` 110 | 2. Reload the VSCode window by running `Developer: Reload Window` from the command palette. 111 | 112 | ### Project Setup 113 | 114 | ```sh 115 | npm install 116 | ``` 117 | 118 | #### Compile and Hot-Reload for Development 119 | 120 | ```sh 121 | npm run dev 122 | ``` 123 | 124 | #### Type-Check, Compile and Minify for Production 125 | 126 | ```sh 127 | npm run build 128 | ``` 129 | 130 | #### Run Unit Tests with [Vitest](https://vitest.dev/) 131 | 132 | ```sh 133 | npm run test:unit 134 | ``` 135 | 136 | #### Lint with [ESLint](https://eslint.org/) 137 | 138 | ```sh 139 | npm run lint 140 | ``` 141 | -------------------------------------------------------------------------------- /env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Basic Dataview Query Builder 10 | 11 | 30 | 31 | 32 | 33 |
34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basic-dataview-query-builder", 3 | "version": "1.2.5", 4 | "type": "module", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "run-p type-check build-only", 8 | "preview": "vite preview --port 4173", 9 | "test:unit": "vitest --environment jsdom", 10 | "build-only": "vite build --base=/basic-dataview-query-builder/", 11 | "type-check": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false", 12 | "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore" 13 | }, 14 | "dependencies": { 15 | "@fortawesome/fontawesome-free": "^6.5.2", 16 | "bulma": "^0.9.4", 17 | "lodash.clonedeep": "^4.5.0", 18 | "pinia": "^2.1.7", 19 | "vue": "^3.3.0", 20 | "vue-router": "^4.3.2" 21 | }, 22 | "devDependencies": { 23 | "@rushstack/eslint-patch": "^1.10.3", 24 | "@types/jsdom": "^21.1.7", 25 | "@types/node": "^20.14.2", 26 | "@vitejs/plugin-vue": "^5.0.5", 27 | "@vue/eslint-config-prettier": "^9.0.0", 28 | "@vue/eslint-config-typescript": "^13.0.0", 29 | "@vue/test-utils": "^2.4.6", 30 | "@vue/tsconfig": "^0.5.1", 31 | "eslint": "^8.57.0", 32 | "eslint-plugin-vue": "^9.26.0", 33 | "jsdom": "^22.1.0", 34 | "npm-run-all": "^4.1.5", 35 | "prettier": "^3.3.1", 36 | "sass": "^1.77.4", 37 | "typescript": "^5.4.5", 38 | "vite": "^5.2.13", 39 | "vitest": "^1.6.0", 40 | "vue-tsc": "^2.0.21" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-blu/basic-dataview-query-builder/9877765011964f414f795b2cab9b0dd5428d12d8/public/favicon.ico -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | 15 | 26 | -------------------------------------------------------------------------------- /src/assets/header.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 36 | 38 | 43 | 47 | 50 | 53 | 56 | 59 | 62 | 65 | 68 | 71 | 74 | 77 | 80 | 83 | 84 | 88 | 92 | 96 | 100 | 104 | 108 | 109 | 114 | 118 | 122 | 126 | 130 | 134 | 138 | 142 | 146 | 147 | 148 | 149 | -------------------------------------------------------------------------------- /src/assets/main.css: -------------------------------------------------------------------------------- 1 | @import "bulma/css/bulma.css"; 2 | @import "@fortawesome/fontawesome-free/css/fontawesome.css"; 3 | @import "@fortawesome/fontawesome-free/css/solid.css"; 4 | @import "@fortawesome/fontawesome-free/css/brands.css"; 5 | 6 | :root { 7 | --color-background: #fff; 8 | --color-boxes: #e6ebf0; 9 | --color-text: #2c3e50; 10 | --color-text-interactive: #00b3b7; 11 | --color-text-hint: #507192; 12 | --color-text-hint-hover: #67a2dd; 13 | --size-font-info: 14pt; 14 | } 15 | 16 | *, 17 | *::before, 18 | *::after { 19 | box-sizing: border-box; 20 | margin: 0; 21 | position: relative; 22 | font-weight: normal; 23 | } 24 | 25 | body { 26 | min-height: 100vh; 27 | color: var(--color-text); 28 | background: var(--color-background); 29 | line-height: 1.6; 30 | font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, 31 | Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", 32 | sans-serif; 33 | font-size: 15px; 34 | text-rendering: optimizeLegibility; 35 | -webkit-font-smoothing: antialiased; 36 | -moz-osx-font-smoothing: grayscale; 37 | } 38 | 39 | #app { 40 | width: 100%; 41 | margin: 0 auto; 42 | padding: 2rem; 43 | 44 | font-weight: normal; 45 | } 46 | 47 | .button.is-ghost { 48 | color: var(--color-text-hint); 49 | } 50 | .button.is-ghost:hover { 51 | text-decoration: none; 52 | color: var(--color-text-interactive); 53 | } 54 | 55 | a { 56 | color: var(--color-text-interactive); 57 | } 58 | 59 | .message .message-header { 60 | background-color: var(--color-text-hint); 61 | } 62 | .message { 63 | background-color: var(--color-boxes); 64 | } 65 | 66 | .message .message-body { 67 | border-color: var(--color-text-hint); 68 | } 69 | -------------------------------------------------------------------------------- /src/assets/mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin box { 2 | background-color: var(--color-boxes); 3 | border-radius: 3px; 4 | padding: 1em; 5 | } 6 | 7 | @mixin heading { 8 | text-transform: uppercase; 9 | font-weight: bolder; 10 | font-size: 12pt; 11 | color: var(--color-text-hint); 12 | } -------------------------------------------------------------------------------- /src/assets/questions.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "question": "How do you want to see the results of your query?", 4 | "subtitle": "Query Type", 5 | "answers": [ 6 | { 7 | "label": "I want to have a LIST of note links matching my query", 8 | "dataview": "LIST", 9 | "readmore": "https://blacksmithgu.github.io/obsidian-dataview/queries/query-types/#list" 10 | }, 11 | { 12 | "label": "I want to have a TABLE of my note links with additional data as columns for each result", 13 | "dataview": "TABLE {{tablecolumns}}", 14 | "readmore": "https://blacksmithgu.github.io/obsidian-dataview/queries/query-types/#table", 15 | "extras": [ 16 | { 17 | "type": "hint", 18 | "message": "Please give a comma separated list of meta data that represents each column." 19 | }, 20 | { 21 | "type": "hint", 22 | "title": "Meta data keys with spaces", 23 | "message": "If your meta data key contain spaces, you cannot use it as-is. It won't get recognized. Please replace all spaces with dashes and write it lower case, i.e. if your key is 'Todays Good Deeds', use 'todays-good-deeds' in dataview queries." 24 | }, 25 | { 26 | "type": "input", 27 | "label": "Table columns (comma separated)", 28 | "varname": "tablecolumns" 29 | } 30 | ] 31 | }, 32 | { 33 | "label": "I want to have a TASK list that summarizes my tasks", 34 | "dataview": "TASK", 35 | "readmore": "https://blacksmithgu.github.io/obsidian-dataview/queries/query-types/#task" 36 | }, 37 | { 38 | "type": "input", 39 | "label": "I want to see a CALENDAR which shows each note as a dot", 40 | "dataview": "CALENDAR {{datefield}}", 41 | "readmore": "https://blacksmithgu.github.io/obsidian-dataview/queries/query-types/#calendar", 42 | "extras": [ 43 | { 44 | "type": "hint", 45 | "message": "Each note needs a valid date meta data field to show in CALENDAR view, otherwise the query won't render. Good picks are i.e. file.ctime or file.mtime. The date meta data field determines on which day the note gets displayed." 46 | }, 47 | { 48 | "type": "input", 49 | "label": "Which date field should be used?", 50 | "variabletype": "metadata", 51 | "varname": "datefield" 52 | } 53 | ] 54 | } 55 | ] 56 | }, 57 | { 58 | "question": "Do you want to include all notes in your vault or only those from a particular source?", 59 | "subtitle": "Page sources", 60 | "answers": [ 61 | { 62 | "label": "I want to include all notes", 63 | "dataview": "" 64 | }, 65 | { 66 | "label": "I only want to show notes that have a particular tag", 67 | "dataview": "FROM #{{tagName}}", 68 | "readmore": "https://blacksmithgu.github.io/obsidian-dataview/queries/data-commands/#from", 69 | "extras": [ 70 | { 71 | "type": "input", 72 | "label": "Name of the tag: #", 73 | "varname": "tagName" 74 | } 75 | ] 76 | }, 77 | { 78 | "label": "I only want to show notes inside a specific root-level folder (and its subfolders)", 79 | "dataview": "FROM \"{{folderName}}\"", 80 | "readmore": "https://blacksmithgu.github.io/obsidian-dataview/queries/data-commands/#from", 81 | "extras": [ 82 | { 83 | "type": "input", 84 | "label": "Name of the folder", 85 | "varname": "folderName" 86 | } 87 | ] 88 | }, 89 | { 90 | "label": "I only want to show notes inside a specific subfolder", 91 | "dataview": "FROM \"{{path/to/folderName}}\"", 92 | "readmore": "https://blacksmithgu.github.io/obsidian-dataview/queries/data-commands/#from", 93 | "extras": [ 94 | { 95 | "type": "hint", 96 | "message": "Give the path starting from and exclusive your vault folder, separated by slashes (/)" 97 | }, 98 | { 99 | "type": "input", 100 | "label": "Path to folder from vault root", 101 | "varname": "path/to/folderName" 102 | } 103 | ] 104 | }, 105 | { 106 | "label": "I only want to show notes that link to my current note", 107 | "dataview": "FROM [[]]", 108 | "readmore": "https://blacksmithgu.github.io/obsidian-dataview/queries/data-commands/#from" 109 | }, 110 | { 111 | "label": "I only want to show notes that my current note links to", 112 | "dataview": "FROM outgoing([[]])", 113 | "readmore": "https://blacksmithgu.github.io/obsidian-dataview/queries/data-commands/#from" 114 | }, 115 | { 116 | "label": "I only want to show notes that link to a specific note", 117 | "dataview": "FROM [[{{NoteName}}]]", 118 | "readmore": "https://blacksmithgu.github.io/obsidian-dataview/queries/data-commands/#from", 119 | "extras": [ 120 | { 121 | "type": "input", 122 | "label": "Name of note", 123 | "varname": "NoteName" 124 | } 125 | ] 126 | }, 127 | { 128 | "label": "I only want to show notes that are linked to from a specific note", 129 | "dataview": "FROM outgoing([[{{NoteName}}]])", 130 | "readmore": "https://blacksmithgu.github.io/obsidian-dataview/queries/data-commands/#from", 131 | "extras": [ 132 | { 133 | "type": "input", 134 | "label": "Name of note", 135 | "varname": "NoteName" 136 | } 137 | ] 138 | } 139 | ] 140 | }, 141 | { 142 | "question": "Do you want to filter your results further based on the notes metadata?", 143 | "subtitle": "Filter pages", 144 | "answers": [ 145 | { 146 | "label": "No, I want to see all notes returned by my FROM statement", 147 | "dataview": "" 148 | }, 149 | { 150 | "label": "I only want to see notes where a particular field exists", 151 | "dataview": "WHERE {{nameOfField}}", 152 | "readmore": "https://blacksmithgu.github.io/obsidian-dataview/queries/data-commands/#where", 153 | "extras": [ 154 | { 155 | "type": "input", 156 | "label": "Name of field", 157 | "varname": "nameOfField" 158 | } 159 | ] 160 | }, 161 | { 162 | "label": "I only want to see notes where a particular field equals a certain value (number, string, date, etc)", 163 | "dataview": "WHERE {{nameOfField}} = ", 164 | "readmore": "https://blacksmithgu.github.io/obsidian-dataview/queries/data-commands/#where", 165 | "extras": [ 166 | { 167 | "type": "input", 168 | "label": "Name of field", 169 | "variabletype": "metadata", 170 | "varname": "nameOfField" 171 | } 172 | ] 173 | }, 174 | { 175 | "label": "I only want to see notes where a particular field contains certain value (inside an array, or as part of a string)", 176 | "dataview": "WHERE contains({{nameOfMetadata}}, \"{{value}}\")", 177 | "readmore": "https://blacksmithgu.github.io/obsidian-dataview/reference/functions/#containsobjectliststring-value", 178 | "extras": [ 179 | { 180 | "type": "hint", 181 | "message": "Using the contains function allows you to either search for a substring in texts, i.e. it'll find 'Lorem ipsum' if you search for 'Lor' or to search a multi-value field for one element. Hit the ? on the answer to read more about contains()." 182 | }, 183 | { 184 | "type": "input", 185 | "label": "Name of field", 186 | "variabletype": "metadata", 187 | "varname": "nameOfMetadata" 188 | }, 189 | { 190 | "type": "input", 191 | "label": "Value to check for", 192 | "varname": "value" 193 | } 194 | ] 195 | } 196 | ] 197 | }, 198 | { 199 | "question": "Against which value type you want to compare?", 200 | "subtitle": "Field Type Compare", 201 | "condition": "WHERE =", 202 | "appendix": ".-1", 203 | "answers": [ 204 | { 205 | "label": "I want to compare to a number", 206 | "dataview": "{{value}}", 207 | "readmore": "https://blacksmithgu.github.io/obsidian-dataview/reference/literals/", 208 | "extras": [ 209 | { 210 | "type": "input", 211 | "label": "Number to check against", 212 | "varname": "value" 213 | } 214 | ] 215 | }, 216 | { 217 | "label": "I want to compare to a text value", 218 | "dataview": "\"{{value}}\"", 219 | "readmore": "https://blacksmithgu.github.io/obsidian-dataview/reference/literals/", 220 | "extras": [ 221 | { 222 | "type": "input", 223 | "label": "Text to check against", 224 | "varname": "value" 225 | } 226 | ] 227 | }, 228 | { 229 | "label": "I want to compare to a date", 230 | "dataview": "date(\"{{value}}\")", 231 | "readmore": "https://blacksmithgu.github.io/obsidian-dataview/reference/literals/#dates", 232 | "extras": [ 233 | { 234 | "type": "hint", 235 | "message": "Specify the date you want to compare against in ISO format, i.e. 2020-05-30 or 2020-12-24T18:00. Click the ? icon on the answer option to see more input possibilities." 236 | }, 237 | { 238 | "type": "input", 239 | "label": "Date to check against", 240 | "varname": "value" 241 | } 242 | ] 243 | }, 244 | { 245 | "label": "I want to compare to a note link", 246 | "dataview": "[[{{value}}]]", 247 | "readmore": "https://blacksmithgu.github.io/obsidian-dataview/reference/literals/", 248 | "extras": [ 249 | { 250 | "type": "input", 251 | "label": "Name of file to check against", 252 | "varname": "value" 253 | } 254 | ] 255 | } 256 | ] 257 | }, 258 | { 259 | "question": "How do you want the results sorted?", 260 | "subtitle": "Sorting", 261 | "answers": [ 262 | { 263 | "label": "Ascending after file path or group key, if I choose to group next (default behaviour)", 264 | "dataview": "" 265 | }, 266 | { 267 | "label": "Based on the note name", 268 | "dataview": "SORT file.name", 269 | "readmore": "https://blacksmithgu.github.io/obsidian-dataview/queries/data-commands/#sort" 270 | }, 271 | { 272 | "label": "Based on the note's creation date", 273 | "dataview": "SORT file.ctime", 274 | "readmore": "https://blacksmithgu.github.io/obsidian-dataview/queries/data-commands/#sort" 275 | }, 276 | { 277 | "label": "Based on my own metadata field", 278 | "dataview": "SORT {{nameOfField}}", 279 | "readmore": "https://blacksmithgu.github.io/obsidian-dataview/reference/functions/#sortlist", 280 | "extras": [ 281 | { 282 | "type": "input", 283 | "label": "Field name to sort after", 284 | "variabletype": "metadata", 285 | "varname": "nameOfField" 286 | } 287 | ] 288 | } 289 | ] 290 | }, 291 | { 292 | "question": "In which direction you want your results sorted?", 293 | "subtitle": "Sort Direction", 294 | "condition": "SORT", 295 | "appendix": ".-1", 296 | "answers": [ 297 | { 298 | "label": "Ascending (A-Z, 0-9, oldest to newest)", 299 | "dataview": "ASC", 300 | "readmore": "https://blacksmithgu.github.io/obsidian-dataview/queries/data-commands/#sort" 301 | }, 302 | { 303 | "label": "Descending (Z-A, 9-0, newest to oldest)", 304 | "dataview": "DESC", 305 | "readmore": "https://blacksmithgu.github.io/obsidian-dataview/queries/data-commands/#sort" 306 | } 307 | ] 308 | }, 309 | { 310 | "question": "Do you want similar results grouped together based on one of their metadata values?", 311 | "subtitle": "Grouping", 312 | "condition": "-TASK", 313 | "answers": [ 314 | { 315 | "label": "No, I want one result line per note", 316 | "dataview": "" 317 | }, 318 | { 319 | "label": "Group based on the folder they belong to.", 320 | "dataview": "GROUP BY file.folder", 321 | "readmore": "https://blacksmithgu.github.io/obsidian-dataview/queries/data-commands/#group-by", 322 | "extras": [ 323 | { 324 | "type": "hint", 325 | "message": "After grouping, you need to prepend a 'rows.' to all fields you want to have as output and all data commands that come after GROUP BY. The Query Builder does take care of that for you." 326 | } 327 | ] 328 | }, 329 | { 330 | "label": "Group based on the tags they have.", 331 | "dataview": "FLATTEN file.etags AS tags GROUP BY tags", 332 | "readmore": "https://blacksmithgu.github.io/obsidian-dataview/queries/data-commands/#group-by", 333 | "extras": [ 334 | { 335 | "type": "hint", 336 | "message": "After grouping, you need to prepend a 'rows.' to all fields you want to have as output and all data commands that come after GROUP BY. The Query Builder does take care of that for you." 337 | }, 338 | { 339 | "type": "hint", 340 | "message": "The tags of a file are a multivalue field. When grouping after a multivalue field, you need to FLATTEN it first, so that every single value of the multivalue field becomes its own group." 341 | } 342 | ] 343 | }, 344 | { 345 | "label": "Group based on the month they were created.", 346 | "dataview": "GROUP BY dateformat(file.ctime, \"yyyy-MM\")", 347 | "readmore": "https://blacksmithgu.github.io/obsidian-dataview/queries/data-commands/#group-by", 348 | "extras": [ 349 | { 350 | "type": "hint", 351 | "message": "After grouping, you need to prepend a 'rows.' to all fields you want to have as output and all data commands that come after GROUP BY. The Query Builder does take care of that for you." 352 | } 353 | ] 354 | }, 355 | { 356 | "label": "Group based on my own single value metadata field", 357 | "dataview": "GROUP BY {{nameOfField}}", 358 | "readmore": "https://blacksmithgu.github.io/obsidian-dataview/queries/data-commands/#group-by", 359 | "extras": [ 360 | { 361 | "type": "input", 362 | "label": "Field name to group after", 363 | "variabletype": "metadata", 364 | "varname": "nameOfField" 365 | }, 366 | { 367 | "type": "hint", 368 | "message": "After grouping, you need to prepend a 'rows.' to all fields you want to have as output and all data commands that come after GROUP BY. The Query Builder does take care of that for you." 369 | } 370 | ] 371 | }, 372 | { 373 | "label": "Group based on my own multi value metadata field", 374 | "dataview": "FLATTEN {{nameOfField}} AS groups GROUP BY groups", 375 | "readmore": "https://blacksmithgu.github.io/obsidian-dataview/queries/data-commands/#group-by", 376 | "extras": [ 377 | { 378 | "type": "input", 379 | "label": "Field name to group after", 380 | "variabletype": "metadata", 381 | "varname": "nameOfField" 382 | }, 383 | { 384 | "type": "hint", 385 | "message": "After grouping, you need to prepend a 'rows.' to all fields you want to have as output and all data commands that come after GROUP BY. The Query Builder does take care of that for you." 386 | }, 387 | { 388 | "type": "hint", 389 | "message": "When grouping after a multivalue field, you need to FLATTEN it first, so that every single value of the multivalue field becomes its own group." 390 | } 391 | ] 392 | } 393 | ] 394 | }, 395 | { 396 | "question": "Do you want similar results grouped together based on one of their metadata values?", 397 | "subtitle": "Grouping", 398 | "condition": "TASK", 399 | "answers": [ 400 | { 401 | "label": "No, I want one result line per note", 402 | "dataview": "" 403 | }, 404 | { 405 | "label": "Yes, based on the file they came from", 406 | "dataview": "GROUP BY file.link", 407 | "readmore": "https://blacksmithgu.github.io/obsidian-dataview/queries/data-commands/#group-by", 408 | "extras": [ 409 | { 410 | "type": "hint", 411 | "message": "After grouping, you need to prepend a 'rows.' to all fields you want to have as output and all data commands that come after GROUP BY. The Query Builder does take care of that for you." 412 | } 413 | ] 414 | }, 415 | { 416 | "label": "Group based on the folder they belong to.", 417 | "dataview": "GROUP BY file.folder", 418 | "readmore": "https://blacksmithgu.github.io/obsidian-dataview/queries/data-commands/#group-by", 419 | "extras": [ 420 | { 421 | "type": "hint", 422 | "message": "After grouping, you need to prepend a 'rows.' to all fields you want to have as output and all data commands that come after GROUP BY. The Query Builder does take care of that for you." 423 | } 424 | ] 425 | }, 426 | { 427 | "label": "Group based on the tags they have.", 428 | "dataview": "FLATTEN file.etags AS tags GROUP BY tags", 429 | "readmore": "https://blacksmithgu.github.io/obsidian-dataview/queries/data-commands/#group-by", 430 | "extras": [ 431 | { 432 | "type": "hint", 433 | "message": "After grouping, you need to prepend a 'rows.' to all fields you want to have as output and all data commands that come after GROUP BY. The Query Builder does take care of that for you." 434 | }, 435 | { 436 | "type": "hint", 437 | "message": "The tags of a file are a multivalue field. When grouping after a multivalue field, you need to FLATTEN it first, so that every single value of the multivalue field becomes its own group." 438 | } 439 | ] 440 | }, 441 | { 442 | "label": "Group based on the month they were created.", 443 | "dataview": "GROUP BY dateformat(file.ctime, \"yyyy-MM\")", 444 | "readmore": "https://blacksmithgu.github.io/obsidian-dataview/queries/data-commands/#group-by", 445 | "extras": [ 446 | { 447 | "type": "hint", 448 | "message": "After grouping, you need to prepend a 'rows.' to all fields you want to have as output and all data commands that come after GROUP BY. The Query Builder does take care of that for you." 449 | } 450 | ] 451 | }, 452 | { 453 | "label": "Group based on my own single value metadata field", 454 | "dataview": "GROUP BY {{nameOfField}}", 455 | "readmore": "https://blacksmithgu.github.io/obsidian-dataview/queries/data-commands/#group-by", 456 | "extras": [ 457 | { 458 | "type": "input", 459 | "label": "Field name to group after", 460 | "variabletype": "metadata", 461 | "varname": "nameOfField" 462 | }, 463 | { 464 | "type": "hint", 465 | "message": "After grouping, you need to prepend a 'rows.' to all fields you want to have as output and all data commands that come after GROUP BY. The Query Builder does take care of that for you." 466 | } 467 | ] 468 | }, 469 | { 470 | "label": "Group based on my own multi value metadata field", 471 | "dataview": "FLATTEN {{nameOfField}} AS groups GROUP BY groups", 472 | "readmore": "https://blacksmithgu.github.io/obsidian-dataview/queries/data-commands/#group-by", 473 | "extras": [ 474 | { 475 | "type": "input", 476 | "label": "Field name to group after", 477 | "variabletype": "metadata", 478 | "varname": "nameOfField" 479 | }, 480 | { 481 | "type": "hint", 482 | "message": "After grouping, you need to prepend a 'rows.' to all fields you want to have as output and all data commands that come after GROUP BY. The Query Builder does take care of that for you." 483 | }, 484 | { 485 | "type": "hint", 486 | "message": "When grouping after a multivalue field, you need to FLATTEN it first, so that every single value of the multivalue field becomes its own group." 487 | } 488 | ] 489 | } 490 | ] 491 | }, 492 | { 493 | "question": "Do you want every result, or just the first few matches?", 494 | "subtitle": "Set a limit", 495 | "answers": [ 496 | { 497 | "label": "Show me everything", 498 | "dataview": "" 499 | }, 500 | { 501 | "label": "Show me only the first few matching results", 502 | "dataview": "LIMIT {{numberOfResults}}", 503 | "readmore": "https://blacksmithgu.github.io/obsidian-dataview/queries/data-commands/#limit", 504 | "extras": [ 505 | { 506 | "type": "input", 507 | "label": "Limit count", 508 | "varname": "numberOfResults" 509 | } 510 | ] 511 | } 512 | ] 513 | }, 514 | { 515 | "question": "Do you want to see an additional information alongside your file links on your LIST?", 516 | "condition": "LIST -GROUP", 517 | "appendix": "0", 518 | "subtitle": "Additional List Field", 519 | "answers": [ 520 | { 521 | "label": "No, I only want to see my file links", 522 | "dataview": "", 523 | "readmore": "https://blacksmithgu.github.io/obsidian-dataview/queries/query-types/#list" 524 | }, 525 | { 526 | "label": "Show a meta data value alongside the file links", 527 | "dataview": "{{listfield}}", 528 | "readmore": "https://blacksmithgu.github.io/obsidian-dataview/queries/query-types/#list", 529 | "extras": [ 530 | { 531 | "type": "input", 532 | "variabletype": "metadata", 533 | "label": "Meta data to output", 534 | "varname": "listfield" 535 | } 536 | ] 537 | } 538 | ] 539 | }, 540 | { 541 | "question": "Do you want to see an additional information alongside your groups on your LIST?", 542 | "condition": "LIST GROUP", 543 | "appendix": "0", 544 | "subtitle": "Additional List Field", 545 | "answers": [ 546 | { 547 | "label": "No, I only want to see my groups", 548 | "dataview": "", 549 | "readmore": "https://blacksmithgu.github.io/obsidian-dataview/queries/data-commands/#group-by" 550 | }, 551 | { 552 | "label": "I want to see all note links belonging to this group", 553 | "dataview": "file.link", 554 | "readmore": "https://blacksmithgu.github.io/obsidian-dataview/queries/data-commands/#group-by" 555 | }, 556 | { 557 | "label": "I want to see all note names belonging to this group", 558 | "dataview": "file.name", 559 | "readmore": "https://blacksmithgu.github.io/obsidian-dataview/queries/data-commands/#group-by" 560 | } 561 | ] 562 | } 563 | ] 564 | -------------------------------------------------------------------------------- /src/components/answeroption-extras/answer-extra-hint.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/components/answeroption-extras/answer-extra-input.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /src/components/dqb-answeroption-extras.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 38 | 39 | 44 | -------------------------------------------------------------------------------- /src/components/dqb-answeroption.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 40 | 41 | 75 | -------------------------------------------------------------------------------- /src/components/dqb-dataviewQuery.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 66 | 67 | 105 | -------------------------------------------------------------------------------- /src/components/dqb-footer.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 51 | 52 | 74 | -------------------------------------------------------------------------------- /src/components/dqb-header.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | 11 | 17 | -------------------------------------------------------------------------------- /src/components/dqb-navigation.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 72 | 73 | 80 | -------------------------------------------------------------------------------- /src/components/dqb-question.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 60 | 61 | 78 | -------------------------------------------------------------------------------- /src/components/dqb-routerButton.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/interfaces/question.ts: -------------------------------------------------------------------------------- 1 | export interface Question { 2 | question: string; 3 | subtitle: string; 4 | answers: Array; 5 | selected: SelectedAnswer; 6 | multiselect?: string; 7 | condition?: string; 8 | appendix?: string; 9 | } 10 | 11 | export interface SelectedAnswer { 12 | dataview: string; 13 | index: number; 14 | answer: AnswerOption; 15 | 16 | rawDataview: string; 17 | variables?: { [id: string]: string }; 18 | appendixDataviews?: Array; 19 | } 20 | 21 | export interface AnswerOption { 22 | label: string; 23 | dataview: string; 24 | 25 | type?: "Input"; 26 | inputs?: Array; 27 | } 28 | 29 | export interface AnswerInputs { 30 | label: string; 31 | varname: string; 32 | 33 | preset?: string; 34 | } 35 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | import App from "./App.vue"; 3 | import router from "./router"; 4 | import { createPinia } from "pinia"; 5 | 6 | import "./assets/main.css"; 7 | 8 | const pinia = createPinia(); 9 | const app = createApp(App); 10 | 11 | app.use(router); 12 | app.use(pinia); 13 | app.mount("#app"); 14 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from "vue-router"; 2 | import StartView from "../views/StartView.vue"; 3 | 4 | const router = createRouter({ 5 | history: createWebHistory(import.meta.env.BASE_URL), 6 | routes: [ 7 | { 8 | path: "/", 9 | name: "home", 10 | component: StartView, 11 | }, 12 | { 13 | path: "/questions", 14 | name: "question", 15 | component: () => import("../views/QuestionView.vue"), 16 | }, 17 | { 18 | path: "/result", 19 | name: "result", 20 | component: () => import("../views/ResultView.vue"), 21 | }, 22 | { 23 | path: "/privacy", 24 | name: "privacy", 25 | component: () => import("../views/PrivacyView.vue"), 26 | }, 27 | ], 28 | }); 29 | 30 | export default router; 31 | -------------------------------------------------------------------------------- /src/stores/questions.store.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from "pinia"; 2 | import cloneDeep from "lodash.clonedeep"; 3 | import initalQuestions from "@/assets/questions.json"; 4 | import type { AnswerOption, Question } from "./../interfaces/question"; 5 | import { 6 | replacePlaceholdersInQueryString, 7 | handleGroupByCommand, 8 | enhanceWithAppendixes, 9 | addAppendix, 10 | } from "@/utilities/dataviewQuery.utility"; 11 | import { doesFulfillCondition } from "@/utilities/conditionString.utility"; 12 | 13 | export const useQuestionsStore = defineStore("questionsStore", { 14 | state: () => ({ 15 | questions: initalQuestions as Array, 16 | currentQuestionIndex: 0, 17 | }), 18 | getters: { 19 | currentQuestion: (state) => state.questions[state.currentQuestionIndex], 20 | queryParts: (state) => 21 | state.questions 22 | .filter((q) => q.selected?.dataview) 23 | .map((q) => q.selected?.dataview), 24 | questionsLength: (state) => state.questions.length, 25 | isLastQuestion: (state) => { 26 | if (!state.questions) return false; 27 | if (state.currentQuestionIndex + 1 === state.questionsLength) { 28 | return true; 29 | } 30 | 31 | let tempIndex = state.currentQuestionIndex; 32 | while ( 33 | tempIndex < state.questionsLength - 1 && 34 | !doesFulfillCondition( 35 | state.queryParts, 36 | state.questions[tempIndex]?.condition, 37 | ) 38 | ) { 39 | tempIndex++; 40 | } 41 | 42 | return tempIndex + 1 === state.questionsLength; 43 | }, 44 | computedQueryParts: (state) => { 45 | const questions = cloneDeep(state.questions); 46 | enhanceWithAppendixes(questions, state.queryParts); 47 | 48 | const queryParts = questions 49 | .map((q) => replacePlaceholdersInQueryString(q)) 50 | .map((q) => addAppendix(q)); 51 | 52 | handleGroupByCommand(queryParts); 53 | 54 | return queryParts.map((q) => q.selected?.dataview || ""); 55 | }, 56 | computedQuery: (state) => { 57 | return state.computedQueryParts.reduce( 58 | (acc, curr) => (curr ? `${acc}${acc ? "\n" : ""}${curr}` : acc), 59 | "", 60 | ); 61 | }, 62 | }, 63 | actions: { 64 | moveForward() { 65 | let tempIndex = this.currentQuestionIndex + 1; 66 | 67 | while ( 68 | !doesFulfillCondition( 69 | this.queryParts, 70 | this.questions[tempIndex]?.condition, 71 | ) 72 | ) { 73 | tempIndex++; 74 | } 75 | if (tempIndex < this.questionsLength) { 76 | this.currentQuestionIndex = tempIndex; 77 | return true; 78 | } 79 | 80 | return false; 81 | }, 82 | moveBack() { 83 | if (this.currentQuestionIndex === 0) return; 84 | 85 | let tempIndex = this.currentQuestionIndex - 1; 86 | 87 | while ( 88 | !doesFulfillCondition( 89 | this.queryParts, 90 | this.questions[tempIndex].condition, 91 | ) 92 | ) { 93 | tempIndex--; 94 | } 95 | this.currentQuestionIndex = tempIndex; 96 | }, 97 | resetAppState() { 98 | this.questions.forEach((q) => (q.selected = undefined)); 99 | this.currentQuestionIndex = 0; 100 | }, 101 | setSelected(question: Question, index: any, answer: AnswerOption) { 102 | question.selected = { 103 | index: index, 104 | answer: answer, 105 | dataview: answer.dataview, 106 | rawDataview: answer.dataview, 107 | }; 108 | }, 109 | updateAnswerVariableMap( 110 | question: Question, 111 | variableName: string, 112 | value: string, 113 | ): void { 114 | if (!question.selected) { 115 | console.error( 116 | "Cannot update variable map for non existing selected answer", 117 | question, 118 | ); 119 | } 120 | if (!question.selected?.variables) { 121 | question.selected.variables = {}; 122 | } 123 | question.selected.variables[variableName] = value; 124 | }, 125 | }, 126 | }); 127 | -------------------------------------------------------------------------------- /src/utilities/conditionString.utility.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { 3 | doesFulfillCondition, 4 | parseConditionString, 5 | } from "./conditionString.utility"; 6 | 7 | describe("ConditionStrings", () => { 8 | describe("parseConditionString", () => { 9 | it("should return empty object on empty or invalid input", () => { 10 | expect(parseConditionString("")).toEqual({ 11 | mandatory: [], 12 | forbidden: [], 13 | }); 14 | 15 | expect(parseConditionString()).toEqual({ 16 | mandatory: [], 17 | forbidden: [], 18 | }); 19 | }); 20 | it("should parse one word strings correctly", () => { 21 | expect(parseConditionString("FLATTEN")).toEqual({ 22 | mandatory: ["FLATTEN"], 23 | forbidden: [], 24 | }); 25 | 26 | expect(parseConditionString("-FLATTEN")).toEqual({ 27 | mandatory: [], 28 | forbidden: ["FLATTEN"], 29 | }); 30 | }); 31 | 32 | it("should parse two word strings correctly", () => { 33 | expect(parseConditionString("FLATTEN GROUP")).toEqual({ 34 | mandatory: ["FLATTEN", "GROUP"], 35 | forbidden: [], 36 | }); 37 | 38 | expect(parseConditionString("-WHERE -FROM")).toEqual({ 39 | mandatory: [], 40 | forbidden: ["WHERE", "FROM"], 41 | }); 42 | 43 | expect(parseConditionString("WHERE -FROM")).toEqual({ 44 | mandatory: ["WHERE"], 45 | forbidden: ["FROM"], 46 | }); 47 | 48 | expect(parseConditionString("-TABLE SORT")).toEqual({ 49 | mandatory: ["SORT"], 50 | forbidden: ["TABLE"], 51 | }); 52 | }); 53 | 54 | it("should parse strings with OR bindings correctly", () => { 55 | expect(parseConditionString("WHERE~FROM")).toEqual({ 56 | mandatory: [["WHERE", "FROM"]], 57 | forbidden: [], 58 | }); 59 | 60 | expect(parseConditionString("WHERE~FROM TABLE~LIST")).toEqual({ 61 | mandatory: [ 62 | ["WHERE", "FROM"], 63 | ["TABLE", "LIST"], 64 | ], 65 | forbidden: [], 66 | }); 67 | 68 | expect(parseConditionString("WHERE~FROM -TABLE~LIST")).toEqual({ 69 | mandatory: [["WHERE", "FROM"]], 70 | forbidden: [["TABLE", "LIST"]], 71 | }); 72 | }); 73 | 74 | it("should parse complex queries correctly", () => { 75 | expect( 76 | parseConditionString("TABLE~LIST~CALENDAR -FROM -GROUP FLATTEN") 77 | ).toEqual({ 78 | mandatory: [["TABLE", "LIST", "CALENDAR"], "FLATTEN"], 79 | forbidden: ["FROM", "GROUP"], 80 | }); 81 | 82 | expect( 83 | parseConditionString("A B C D -E -F~G~H J~K~L M N -O P -Q~R~S~T") 84 | ).toEqual({ 85 | mandatory: ["A", "B", "C", "D", ["J", "K", "L"], "M", "N", "P"], 86 | forbidden: ["E", ["F", "G", "H"], "O", ["Q", "R", "S", "T"]], 87 | }); 88 | }); 89 | }); 90 | 91 | describe("doesFulfillCondition", () => { 92 | it("should check simple queries and conditions correctly", () => { 93 | const query = ["LIST", "FROM {{folder}}"]; 94 | 95 | expect(doesFulfillCondition(query, "LIST")).toBeTruthy(); 96 | expect(doesFulfillCondition(query, "TABLE")).toBeFalsy(); 97 | 98 | expect(doesFulfillCondition(query, "LIST FROM")).toBeTruthy(); 99 | expect(doesFulfillCondition(query, "LIST -FLATTEN")).toBeTruthy(); 100 | 101 | expect(doesFulfillCondition(query, "LIST -FROM")).toBeFalsy(); 102 | expect(doesFulfillCondition(query, "LIST FLATTEN")).toBeFalsy(); 103 | }); 104 | 105 | it("should check simple queries and conditions with OR correctly", () => { 106 | const query = ["LIST", "FROM {{folder}}"]; 107 | 108 | expect(doesFulfillCondition(query, "WHERE~FROM")).toBeTruthy(); 109 | expect(doesFulfillCondition(query, "TABLE~TASK~LIST")).toBeTruthy(); 110 | expect(doesFulfillCondition(query, "TABLE~LIST~CALENDAR")).toBeTruthy(); 111 | 112 | expect(doesFulfillCondition(query, "WHERE~FLATTEN")).toBeFalsy(); 113 | expect(doesFulfillCondition(query, "TABLE~CALENDAR")).toBeFalsy(); 114 | 115 | expect(doesFulfillCondition(query, "LIST FROM~WHERE")).toBeTruthy(); 116 | expect( 117 | doesFulfillCondition(query, "LIST FROM~WHERE~FLATTEN") 118 | ).toBeTruthy(); 119 | expect( 120 | doesFulfillCondition(query, "FLATTEN~GROUP~LIST FROM") 121 | ).toBeTruthy(); 122 | 123 | expect(doesFulfillCondition(query, "LIST FLATTEN~GROUP")).toBeFalsy(); 124 | expect(doesFulfillCondition(query, "FLATTEN~GROUP FROM")).toBeFalsy(); 125 | }); 126 | 127 | it("should check complex queries and conditions correctly", () => { 128 | const query = [ 129 | "LIST", 130 | "FROM {{folder}}", 131 | "WHERE contains({{field}}, {{value}})", 132 | "FLATTEN file.tasks AS tasks", 133 | 'GROUP BY dateformat(file.ctime, "yyyy-MM") AS Created', 134 | "LIMIT {{limitCount}}", 135 | ]; 136 | 137 | expect(doesFulfillCondition(query, "LIST -TABLE WHERE")).toBeTruthy(); 138 | expect(doesFulfillCondition(query, "LIST -TABLE -SORT")).toBeTruthy(); 139 | expect(doesFulfillCondition(query, "LIST -TABLE~CALENDAR")).toBeTruthy(); 140 | expect( 141 | doesFulfillCondition(query, "LIST FROM WHERE FLATTEN GROUP LIMIT") 142 | ).toBeTruthy(); 143 | expect( 144 | doesFulfillCondition( 145 | query, 146 | "LIST~TABLE WHERE~FROM -SORT LIMIT contains~substring FLATTEN" 147 | ) 148 | ).toBeTruthy(); 149 | 150 | expect( 151 | doesFulfillCondition( 152 | query, 153 | "LIST~TABLE WHERE~FROM SORT LIMIT contains~substring FLATTEN" 154 | ) 155 | ).toBeFalsy(); 156 | expect(doesFulfillCondition(query, "LIST TABLE -SORT")).toBeFalsy(); 157 | }); 158 | }); 159 | }); 160 | -------------------------------------------------------------------------------- /src/utilities/conditionString.utility.ts: -------------------------------------------------------------------------------- 1 | export function parseConditionString(conStr: string) { 2 | const condition = { 3 | mandatory: [], 4 | forbidden: [], 5 | }; 6 | 7 | if (!conStr || conStr.trim() === "") { 8 | return condition; 9 | } 10 | 11 | const parts = conStr.split(" "); 12 | 13 | parts.forEach((p: string) => { 14 | let type = "mandatory"; 15 | p = p.trim(); 16 | 17 | if (p.startsWith("-")) { 18 | type = "forbidden"; 19 | p = p.substring(1); 20 | } 21 | 22 | const con = p.includes("~") ? p.split("~") : p; 23 | condition[type].push(con); 24 | }); 25 | 26 | return condition; 27 | } 28 | 29 | export function doesFulfillCondition( 30 | rawDataviewParts: any[], 31 | conditionString?: string 32 | ) { 33 | if (!conditionString) { 34 | return true; 35 | } 36 | 37 | const conditions = parseConditionString(conditionString); 38 | const query = rawDataviewParts.reduce( 39 | (acc: any, curr: any) => `${acc} ${curr}`, 40 | "" 41 | ); 42 | 43 | for (let i = 0; i < conditions.forbidden.length; i++) { 44 | if (isPresent(conditions.forbidden[i])) { 45 | return false; 46 | } 47 | } 48 | 49 | for (let i = 0; i < conditions.mandatory.length; i++) { 50 | if (isPresent(conditions.mandatory[i])) { 51 | conditions.mandatory.splice(i, 1); 52 | i--; 53 | } 54 | } 55 | 56 | return conditions.mandatory.length === 0; 57 | 58 | function isPresent(conditionPart: String[] | String) { 59 | let isPresent = false; 60 | if (Array.isArray(conditionPart)) { 61 | isPresent = (conditionPart as String[]).some((m) => query.includes(m)); 62 | } else { 63 | isPresent = query.includes(conditionPart); 64 | } 65 | 66 | return isPresent; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/utilities/dataviewQuery.utility.ts: -------------------------------------------------------------------------------- 1 | import type { Question } from "./../interfaces/question"; 2 | import { doesFulfillCondition } from "./conditionString.utility"; 3 | 4 | export function replacePlaceholdersInQueryString( 5 | question: Question, 6 | ignoreAppendixQuestions = true 7 | ) { 8 | if (!question.selected) { 9 | return question; 10 | } 11 | if (question.appendix && ignoreAppendixQuestions) { 12 | question.selected.dataview = ""; 13 | return question; 14 | } 15 | 16 | if (!question.selected.variables) { 17 | return question; 18 | } 19 | 20 | const placeholders = question.selected.rawDataview.matchAll(/{{([^}]+)?}}/g); 21 | 22 | let newDataview = question.selected.rawDataview; 23 | for (const match of placeholders) { 24 | const replacement = question.selected.variables[match[1]]; 25 | if (replacement) { 26 | newDataview = newDataview.replace(match[0], replacement); 27 | } 28 | } 29 | question.selected.dataview = newDataview; 30 | 31 | return question; 32 | } 33 | 34 | export function addAppendix(question: Question) { 35 | if (!question.selected || !question.selected.appendixDataviews) { 36 | return question; 37 | } 38 | 39 | let newDataview = question.selected.dataview; 40 | if (question.selected.appendixDataviews) { 41 | question.selected.appendixDataviews.forEach((ap) => { 42 | if (ap) { 43 | newDataview += " " + ap; 44 | } 45 | }); 46 | } 47 | 48 | question.selected.dataview = newDataview; 49 | return question; 50 | } 51 | 52 | export function enhanceWithAppendixes( 53 | questions: Array, 54 | queryParts: string[] 55 | ) { 56 | if (!questions) return; 57 | 58 | questions.forEach((question, i) => { 59 | if (!question.appendix || !question.selected) return; 60 | 61 | const appI = determineAppendixId(question, i); 62 | if ( 63 | question.condition && 64 | !doesFulfillCondition(queryParts, question.condition) 65 | ) { 66 | if (questions[appI].selected?.appendixDataviews) { 67 | questions[appI].selected.appendixDataviews[i] = null; 68 | } 69 | return; 70 | } 71 | 72 | replacePlaceholdersInQueryString(question, false); 73 | if (questions[appI].selected) { 74 | if (!questions[appI].selected.appendixDataviews) { 75 | questions[appI].selected.appendixDataviews = []; 76 | } 77 | 78 | questions[appI].selected.appendixDataviews[i] = 79 | question.selected.dataview; 80 | } 81 | }); 82 | } 83 | 84 | export function determineAppendixId(question: Question, i: number) { 85 | if (!question?.appendix) return i; 86 | 87 | let appI; 88 | if (question.appendix.startsWith(".")) { 89 | const relative = Number(question.appendix.substring(1)); 90 | appI = i + relative; 91 | } else { 92 | appI = Number(question.appendix); 93 | } 94 | return appI; 95 | } 96 | 97 | export function handleGroupByCommand(questions: Array) { 98 | let groupByIndex; 99 | 100 | questions.forEach((q: Question, index: number) => { 101 | if (q.selected?.dataview.includes("GROUP BY")) groupByIndex = index; 102 | }); 103 | 104 | if (!groupByIndex) { 105 | return; 106 | } 107 | 108 | // 0 is always the query type (LIST/TABLE etc.) and gets executed last, alas needs the prefix 109 | _prependRows(questions[0]); 110 | 111 | // only commands after the group by need the rows. prefix 112 | for (let i = groupByIndex + 1; i < questions.length; i++) { 113 | _prependRows(questions[i]); 114 | } 115 | 116 | function _prependRows(question: Question) { 117 | if (!question.selected) return; 118 | // in case of multiple meta data fields, split by , 119 | const parts = question.selected.dataview.split(","); 120 | const querytypeOrDataCmd = parts[0].split(" ")[0]; 121 | parts[0] = parts[0].replace(querytypeOrDataCmd, ""); 122 | 123 | // Hacky workaround: Don't append on LIMIT numbers. Need to think of something more general here. 124 | if (querytypeOrDataCmd === "LIMIT") return; 125 | 126 | const groupedQuery = []; 127 | for (let i = 0; i < parts.length; i++) { 128 | parts[i] = parts[i].trim(); 129 | const prefix = parts[i] && !parts[i].startsWith("rows.") ? "rows." : ""; 130 | groupedQuery.push(prefix + parts[i]); 131 | } 132 | 133 | question.selected.dataview = 134 | querytypeOrDataCmd + 135 | groupedQuery.reduce((acc, curr) => `${acc}${acc ? "," : ""} ${curr}`, ""); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/views/PrivacyView.vue: -------------------------------------------------------------------------------- 1 | 3 | 4 | 96 | 97 | -------------------------------------------------------------------------------- /src/views/QuestionView.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 19 | 20 | 25 | -------------------------------------------------------------------------------- /src/views/ResultView.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 75 | 76 | 108 | -------------------------------------------------------------------------------- /src/views/StartView.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 51 | 52 | 78 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.json", 3 | "include": ["env.d.ts", "src/**/*", "src/**/*.vue", "src/**/*.json"], 4 | "exclude": ["src/**/__tests__/*"], 5 | "compilerOptions": { 6 | "resolveJsonModule": true, 7 | "composite": true, 8 | "baseUrl": ".", 9 | "paths": { 10 | "@/*": ["./src/*"] 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.dom.json", 3 | "include": ["vite.config.*", "vitest.config.*", "cypress.config.*"], 4 | "compilerOptions": { 5 | "composite": true, 6 | "types": ["node"] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "./tsconfig.config.json" 6 | }, 7 | { 8 | "path": "./tsconfig.app.json" 9 | }, 10 | { 11 | "path": "./tsconfig.vitest.json" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.vitest.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.app.json", 3 | "exclude": [], 4 | "compilerOptions": { 5 | "composite": true, 6 | "lib": [], 7 | "types": ["node", "jsdom"] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from "node:url"; 2 | 3 | import { defineConfig } from "vite"; 4 | import vue from "@vitejs/plugin-vue"; 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | plugins: [vue()], 9 | base: "./", 10 | resolve: { 11 | alias: { 12 | "@": fileURLToPath(new URL("./src", import.meta.url)), 13 | }, 14 | } 15 | }); 16 | --------------------------------------------------------------------------------