├── .babelrc ├── .browserslistrc ├── .editorconfig ├── .eslintrc.js ├── .github └── workflows │ ├── build.yml │ ├── codeql-analysis.yml │ └── release.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── cypress.config.js ├── cypress ├── e2e │ └── examples │ │ ├── actions.spec.js │ │ ├── aliasing.spec.js │ │ ├── assertions.spec.js │ │ ├── connectors.spec.js │ │ ├── cookies.spec.js │ │ ├── cypress_api.spec.js │ │ ├── files.spec.js │ │ ├── local_storage.spec.js │ │ ├── location.spec.js │ │ ├── misc.spec.js │ │ ├── navigation.spec.js │ │ ├── network_requests.spec.js │ │ ├── querying.spec.js │ │ ├── spies_stubs_clocks.spec.js │ │ ├── traversal.spec.js │ │ ├── utilities.spec.js │ │ ├── viewport.spec.js │ │ ├── waiting.spec.js │ │ └── window.spec.js ├── fixtures │ └── example.json ├── plugins │ └── index.js └── support │ ├── commands.js │ └── index.js ├── docs ├── config.schema.md ├── development.md ├── images │ └── responsive.png ├── install.md └── system.md ├── jest.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── favicon.ico ├── img │ └── icons │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon-120x120.png │ │ ├── apple-touch-icon-152x152.png │ │ ├── apple-touch-icon-180x180.png │ │ ├── apple-touch-icon-60x60.png │ │ ├── apple-touch-icon-76x76.png │ │ ├── apple-touch-icon.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── msapplication-icon-144x144.png │ │ ├── mstile-150x150.png │ │ └── safari-pinned-tab.svg ├── index.html ├── manifest.json └── robots.txt ├── src ├── App.vue ├── assets │ ├── logo.png │ └── logo.svg ├── components │ ├── List.vue │ ├── ListColumnTypes.vue │ └── Share.vue ├── layouts │ └── Main.vue ├── main.js ├── plugins │ ├── toasted.js │ └── vuetify.js ├── registerServiceWorker.js ├── router.js ├── services │ ├── Config.js │ └── Database.js └── views │ ├── ChangeFeed.vue │ ├── Config.vue │ ├── Entry.vue │ ├── Home.vue │ ├── ImportExport.vue │ ├── List.vue │ ├── Login.vue │ ├── Search.vue │ └── Tree.vue ├── tests ├── e2e │ ├── .eslintrc.js │ ├── specs │ │ ├── itemEntry.cy.js │ │ ├── itemList.cy.js │ │ ├── login.cy.js │ │ └── search.cy.js │ └── support │ │ ├── commands.js │ │ └── index.js └── unit │ ├── .eslintrc.js │ └── ListColumnTypes.spec.js └── vue.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "transform-es2015-modules-commonjs" 4 | ] 5 | } -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx,vue}] 2 | indent_style = space 3 | indent_size = 2 4 | trim_trailing_whitespace = true 5 | insert_final_newline = true 6 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | 'extends': [ 7 | 'plugin:vue/essential', 8 | '@vue/standard' 9 | ], 10 | rules: { 11 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 12 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 13 | 'no-return-assign': 'off', 14 | 'vue/multi-word-component-names': 'off' 15 | }, 16 | parserOptions: { 17 | 'ecmaVersion': 2020 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | pull_request: 5 | workflow_call: 6 | 7 | jobs: 8 | build: 9 | runs-on: ${{ matrix.os }} 10 | 11 | strategy: 12 | matrix: 13 | os: [ubuntu-latest] 14 | node-version: [17.x] 15 | 16 | steps: 17 | - run: git config --global core.autocrlf false 18 | - uses: actions/checkout@v3 19 | 20 | - name: Use Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v2 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | cache: 'npm' 25 | 26 | - name: Install StoreDown dependencies 27 | run: npm ci 28 | 29 | - name: Run linter 30 | run: npm run lint 31 | 32 | - name: Run e2e tests 33 | id: tests 34 | run: npx vue-cli-service test:e2e --headless 35 | 36 | - name: Get test screenshots 37 | if: failure() && steps.tests.outcome == 'failure' 38 | uses: actions/upload-artifact@v3 39 | with: 40 | name: testScreenshots 41 | path: | 42 | tests/e2e/screenshots 43 | 44 | - name: Get test videos 45 | if: failure() && steps.tests.outcome == 'failure' 46 | uses: actions/upload-artifact@v3 47 | with: 48 | name: testVideo 49 | path: | 50 | tests/e2e/videos 51 | 52 | - name: Build StoreDown 53 | run: npm run build 54 | 55 | - name: Build artifact 56 | uses: actions/upload-artifact@v3 57 | with: 58 | name: dist 59 | path: dist 60 | 61 | - name: Upload release files 62 | uses: actions/upload-artifact@v3 63 | with: 64 | name: release 65 | path: | 66 | package.json 67 | Dockerfile 68 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '32 17 * * 1' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v3 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v2 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | 52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | # queries: security-extended,security-and-quality 54 | 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v2 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 63 | 64 | # If the Autobuild fails above, remove it and uncomment the following three lines. 65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 66 | 67 | # - run: | 68 | # echo "Run, Build Application using script" 69 | # ./location_of_script_within_repo/buildscript.sh 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v2 73 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | uses: ./.github/workflows/build.yml 11 | 12 | release: 13 | runs-on: ubuntu-latest 14 | needs: build 15 | steps: 16 | - name: Retrieve build 17 | uses: actions/download-artifact@v3 18 | with: 19 | name: dist 20 | path: ./dist 21 | - name: Retrieve release files 22 | uses: actions/download-artifact@v3 23 | with: 24 | name: release 25 | path: ./ 26 | - name: Install jq 27 | run: sudo apt-get install -y jq 28 | #Based on https://stackoverflow.com/questions/58177786/get-the-current-pushed-tag-in-github-actions 29 | - name: Get version 30 | run: echo "RELEASE_VERSION=$(jq -r .version ./package.json)" >> $GITHUB_ENV 31 | - name: Test 32 | run: | 33 | echo $RELEASE_VERSION 34 | ls -lt dist 35 | - name: Set up QEMU 36 | uses: docker/setup-qemu-action@v2 37 | - name: Set up Docker Buildx 38 | uses: docker/setup-buildx-action@v2 39 | - name: Login to DockerHub 40 | uses: docker/login-action@v2 41 | with: 42 | username: ${{ secrets.DOCKERHUB_USERNAME }} 43 | password: ${{ secrets.DOCKERHUB_TOKEN }} 44 | - name: Build and push 45 | uses: docker/build-push-action@v3 46 | with: 47 | context: . 48 | platforms: linux/amd64,linux/arm64 49 | push: true 50 | tags: foxusa/storedown:latest,foxusa/storedown:${{ env.RELEASE_VERSION }} 51 | 52 | - name: Zip up release 53 | run: cd dist && zip -r v${{ env.RELEASE_VERSION }}.zip . 54 | 55 | - name: Create GitHub Release 56 | uses: softprops/action-gh-release@v1 57 | with: 58 | name: StoreDown v${{ env.RELEASE_VERSION }} 59 | tag_name: v${{ env.RELEASE_VERSION }} 60 | files: | 61 | dist/v${{ env.RELEASE_VERSION }}.zip 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | /tests/e2e/reports/ 6 | selenium-debug.log 7 | screenshots 8 | videos 9 | 10 | # local env files 11 | .env.local 12 | .env.*.local 13 | 14 | # Log files 15 | npm-debug.log* 16 | yarn-debug.log* 17 | yarn-error.log* 18 | 19 | # Editor directories and files 20 | .idea 21 | .vscode 22 | *.suo 23 | *.ntvs* 24 | *.njsproj 25 | *.sln 26 | *.sw? 27 | home.deploy.md 28 | home.config.yml 29 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx 2 | ADD dist/ /usr/share/nginx/html/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # StoreDown 2 | 3 | ![Responsive to screen size](./docs/images/responsive.png) 4 | > Do not store up for yourselves treasures on earth, where moths and vermin destroy, and where thieves break in and steal. 5 | 6 | I live by the mantra 7 | 8 | > If you cannot find something or cannot put it away, you don't need it. 9 | 10 | StoreDown is a Progressive Web App(PWA) designed to help you put stuff away and find it again. 11 | Due to the way StoreDown was built it can also be a forms application fronting CouchDB.(Like a very simple alternative of Microsoft Access) 12 | 13 | I personally use StoreDown every day to keep track of 1,359 items at time of writing. Those items range from: a 90 pack of dental floss picks, a Jig saw, a refrigerator, to red 15W USB A to USB C cable(of which I have 6). If having this capability appeals to you, come along and enjoy the fun! 14 | 15 | 16 | ## [Demo/Application](https://storedown.org/dist/) 17 | Click `RUN IN LOCAL MODE` in the bottom left to quickly demo/test. 18 | [Here is some test data you can import](https://gist.github.com/FoxUSA/80bc1b72b896a5d1db550ea7aaf4a167) 19 | 20 | > If you use this you should probably take regular backups exports of your data. 21 | 22 | > If you want to sync between multiple devices, you need to setup a [CouchDB server](https://hub.docker.com/_/couchdb). 23 | 24 | ## Features 25 | - BYOS(Bring Your Own Server) 26 | - Touch friendly and mouse friendly ui 27 | - Search 28 | - Can function offline 29 | - CouchDB Sync via PouchDB 30 | - Responsive 31 | - Tags 32 | - Customizable fields 33 | - Data Export(JSON, YML, and CSV) 34 | - Data Import(JSON, and YML) 35 | - TODO markdown 36 | 37 | 38 | 39 | ## Quick Links 40 | - [How to install](./docs/install.md) 41 | - [How to develop](./docs/development.md) 42 | - [Config/Custom field customization](./docs/config.schema.md) 43 | - [How I use StoreDown](./docs/system.md) 44 | - [Sub Reddit](https://www.reddit.com/r/storedown/) 45 | - [Like us on Alternative To][alternativeto] 46 | 47 | 48 | 49 | --- 50 | 51 | ## FAQ 52 | 53 | > Why not just use a spreadsheet? 54 | 55 | The first answer is mobile. Spreadsheets work terrible on mobile. 56 | The second answer is customization. The ability to search via UPC codes is the killer application for this system. 57 | 58 | > So you read "Don't store up" and built an inventory system? 59 | 60 | Keeping track of what you have and storing up are separate things. 61 | Keeping track of things lets you do cool things like see the last time something was used. 62 | Haven't use the rock band drums in 3 years? Might do good for someone else. 63 | This use case alone is why this system is called `StoreDown`. 64 | 65 | Also allows you to not over consume. 66 | Another killer application of this system is just label cables. 67 | - Do I have an unused HDMI cable somewhere? 68 | - I need a USB-A to micro-USB cable thats at least 7 feet long. 69 | 70 | Labeling items allows some unforeseen benefits like: 71 | - Sending someone not technical to retrieve a parallel port. 72 | - Answering questions like: 73 | - When did I buy xyz? 74 | - It's broken. Is it under warranty? 75 | 76 | > I don't like xyz about StoreDown, do you know of any alternatives? 77 | 78 | I would really like to be friends with the folks in orbit around [InvenTree](https://github.com/inventree/InvenTree) and [Grocy](https://github.com/grocy/grocy). 79 | So go check those out. More alternatives on [Alternative To][alternativeto]. 80 | 81 | [alternativeto]: https://alternativeto.net/software/storedown/ -------------------------------------------------------------------------------- /cypress.config.js: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require('cypress') 2 | 3 | module.exports = defineConfig({ 4 | fixturesFolder: 'tests/e2e/fixtures', 5 | screenshotsFolder: 'tests/e2e/screenshots', 6 | videosFolder: 'tests/e2e/videos', 7 | e2e: { 8 | specPattern: 'tests/e2e/specs/**/*.cy.{js,jsx,ts,tsx}', 9 | supportFile: 'tests/e2e/support/index.js', 10 | }, 11 | }) 12 | -------------------------------------------------------------------------------- /cypress/e2e/examples/actions.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context('Actions', () => { 4 | beforeEach(() => { 5 | cy.visit('https://example.cypress.io/commands/actions') 6 | }) 7 | 8 | // https://on.cypress.io/interacting-with-elements 9 | 10 | it('.type() - type into a DOM element', () => { 11 | // https://on.cypress.io/type 12 | cy.get('.action-email') 13 | .type('fake@email.com').should('have.value', 'fake@email.com') 14 | 15 | // .type() with special character sequences 16 | .type('{leftarrow}{rightarrow}{uparrow}{downarrow}') 17 | .type('{del}{selectall}{backspace}') 18 | 19 | // .type() with key modifiers 20 | .type('{alt}{option}') //these are equivalent 21 | .type('{ctrl}{control}') //these are equivalent 22 | .type('{meta}{command}{cmd}') //these are equivalent 23 | .type('{shift}') 24 | 25 | // Delay each keypress by 0.1 sec 26 | .type('slow.typing@email.com', { delay: 100 }) 27 | .should('have.value', 'slow.typing@email.com') 28 | 29 | cy.get('.action-disabled') 30 | // Ignore error checking prior to type 31 | // like whether the input is visible or disabled 32 | .type('disabled error checking', { force: true }) 33 | .should('have.value', 'disabled error checking') 34 | }) 35 | 36 | it('.focus() - focus on a DOM element', () => { 37 | // https://on.cypress.io/focus 38 | cy.get('.action-focus').focus() 39 | .should('have.class', 'focus') 40 | .prev().should('have.attr', 'style', 'color: orange;') 41 | }) 42 | 43 | it('.blur() - blur off a DOM element', () => { 44 | // https://on.cypress.io/blur 45 | cy.get('.action-blur').type('About to blur').blur() 46 | .should('have.class', 'error') 47 | .prev().should('have.attr', 'style', 'color: red;') 48 | }) 49 | 50 | it('.clear() - clears an input or textarea element', () => { 51 | // https://on.cypress.io/clear 52 | cy.get('.action-clear').type('Clear this text') 53 | .should('have.value', 'Clear this text') 54 | .clear() 55 | .should('have.value', '') 56 | }) 57 | 58 | it('.submit() - submit a form', () => { 59 | // https://on.cypress.io/submit 60 | cy.get('.action-form') 61 | .find('[type="text"]').type('HALFOFF') 62 | cy.get('.action-form').submit() 63 | .next().should('contain', 'Your form has been submitted!') 64 | }) 65 | 66 | it('.click() - click on a DOM element', () => { 67 | // https://on.cypress.io/click 68 | cy.get('.action-btn').click() 69 | 70 | // You can click on 9 specific positions of an element: 71 | // ----------------------------------- 72 | // | topLeft top topRight | 73 | // | | 74 | // | | 75 | // | | 76 | // | left center right | 77 | // | | 78 | // | | 79 | // | | 80 | // | bottomLeft bottom bottomRight | 81 | // ----------------------------------- 82 | 83 | // clicking in the center of the element is the default 84 | cy.get('#action-canvas').click() 85 | 86 | cy.get('#action-canvas').click('topLeft') 87 | cy.get('#action-canvas').click('top') 88 | cy.get('#action-canvas').click('topRight') 89 | cy.get('#action-canvas').click('left') 90 | cy.get('#action-canvas').click('right') 91 | cy.get('#action-canvas').click('bottomLeft') 92 | cy.get('#action-canvas').click('bottom') 93 | cy.get('#action-canvas').click('bottomRight') 94 | 95 | // .click() accepts an x and y coordinate 96 | // that controls where the click occurs :) 97 | 98 | cy.get('#action-canvas') 99 | .click(80, 75) // click 80px on x coord and 75px on y coord 100 | .click(170, 75) 101 | .click(80, 165) 102 | .click(100, 185) 103 | .click(125, 190) 104 | .click(150, 185) 105 | .click(170, 165) 106 | 107 | // click multiple elements by passing multiple: true 108 | cy.get('.action-labels>.label').click({ multiple: true }) 109 | 110 | // Ignore error checking prior to clicking 111 | cy.get('.action-opacity>.btn').click({ force: true }) 112 | }) 113 | 114 | it('.dblclick() - double click on a DOM element', () => { 115 | // https://on.cypress.io/dblclick 116 | 117 | // Our app has a listener on 'dblclick' event in our 'scripts.js' 118 | // that hides the div and shows an input on double click 119 | cy.get('.action-div').dblclick().should('not.be.visible') 120 | cy.get('.action-input-hidden').should('be.visible') 121 | }) 122 | 123 | it('.check() - check a checkbox or radio element', () => { 124 | // https://on.cypress.io/check 125 | 126 | // By default, .check() will check all 127 | // matching checkbox or radio elements in succession, one after another 128 | cy.get('.action-checkboxes [type="checkbox"]').not('[disabled]') 129 | .check().should('be.checked') 130 | 131 | cy.get('.action-radios [type="radio"]').not('[disabled]') 132 | .check().should('be.checked') 133 | 134 | // .check() accepts a value argument 135 | cy.get('.action-radios [type="radio"]') 136 | .check('radio1').should('be.checked') 137 | 138 | // .check() accepts an array of values 139 | cy.get('.action-multiple-checkboxes [type="checkbox"]') 140 | .check(['checkbox1', 'checkbox2']).should('be.checked') 141 | 142 | // Ignore error checking prior to checking 143 | cy.get('.action-checkboxes [disabled]') 144 | .check({ force: true }).should('be.checked') 145 | 146 | cy.get('.action-radios [type="radio"]') 147 | .check('radio3', { force: true }).should('be.checked') 148 | }) 149 | 150 | it('.uncheck() - uncheck a checkbox element', () => { 151 | // https://on.cypress.io/uncheck 152 | 153 | // By default, .uncheck() will uncheck all matching 154 | // checkbox elements in succession, one after another 155 | cy.get('.action-check [type="checkbox"]') 156 | .not('[disabled]') 157 | .uncheck().should('not.be.checked') 158 | 159 | // .uncheck() accepts a value argument 160 | cy.get('.action-check [type="checkbox"]') 161 | .check('checkbox1') 162 | .uncheck('checkbox1').should('not.be.checked') 163 | 164 | // .uncheck() accepts an array of values 165 | cy.get('.action-check [type="checkbox"]') 166 | .check(['checkbox1', 'checkbox3']) 167 | .uncheck(['checkbox1', 'checkbox3']).should('not.be.checked') 168 | 169 | // Ignore error checking prior to unchecking 170 | cy.get('.action-check [disabled]') 171 | .uncheck({ force: true }).should('not.be.checked') 172 | }) 173 | 174 | it('.select() - select an option in a