chore: update eslint rules
This commit is contained in:
parent
d604eaa4ef
commit
ac3c831e2b
10
.eslintrc.js
10
.eslintrc.js
|
|
@ -1,10 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
root: true,
|
|
||||||
parser: 'vue-eslint-parser',
|
|
||||||
parserOptions: {
|
|
||||||
parser: '@typescript-eslint/parser',
|
|
||||||
sourceType: 'module',
|
|
||||||
extraFileExtensions: ['.vue'],
|
|
||||||
},
|
|
||||||
extends: '@mutoe/eslint-config-preset-vue',
|
|
||||||
}
|
|
||||||
|
|
@ -6,13 +6,13 @@
|
||||||
version: 2
|
version: 2
|
||||||
updates:
|
updates:
|
||||||
- package-ecosystem: npm
|
- package-ecosystem: npm
|
||||||
directory: "/"
|
directory: /
|
||||||
schedule:
|
schedule:
|
||||||
interval: monthly
|
interval: monthly
|
||||||
allow:
|
allow:
|
||||||
- dependency-type: production
|
- dependency-type: production
|
||||||
|
|
||||||
- package-ecosystem: github-actions
|
- package-ecosystem: github-actions
|
||||||
directory: ".github/workflows"
|
directory: .github/workflows
|
||||||
schedule:
|
schedule:
|
||||||
interval: monthly
|
interval: monthly
|
||||||
|
|
|
||||||
|
|
@ -9,14 +9,14 @@
|
||||||
# the `language` matrix defined below to confirm you have the correct set of
|
# the `language` matrix defined below to confirm you have the correct set of
|
||||||
# supported CodeQL languages.
|
# supported CodeQL languages.
|
||||||
#
|
#
|
||||||
name: "CodeQL"
|
name: CodeQL
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ master ]
|
branches: [master]
|
||||||
pull_request:
|
pull_request:
|
||||||
# The branches below must be a subset of the branches above
|
# The branches below must be a subset of the branches above
|
||||||
branches: [ master ]
|
branches: [master]
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '43 3 * * 6'
|
- cron: '43 3 * * 6'
|
||||||
|
|
||||||
|
|
@ -28,40 +28,40 @@ jobs:
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
language: [ 'javascript' ]
|
language: [javascript]
|
||||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
|
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
|
||||||
# Learn more:
|
# Learn more:
|
||||||
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v2
|
uses: github/codeql-action/init@v2
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
# By default, queries listed here will override any specified in a config file.
|
# By default, queries listed here will override any specified in a config file.
|
||||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||||
|
|
||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@v2
|
uses: github/codeql-action/autobuild@v2
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
# 📚 https://git.io/JvXDl
|
# 📚 https://git.io/JvXDl
|
||||||
|
|
||||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||||
# and modify them (or add more) to build your code if your project
|
# and modify them (or add more) to build your code if your project
|
||||||
# uses a compiled language
|
# uses a compiled language
|
||||||
|
|
||||||
#- run: |
|
# - run: |
|
||||||
# make bootstrap
|
# make bootstrap
|
||||||
# make release
|
# make release
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v2
|
uses: github/codeql-action/analyze@v2
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ jobs:
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20.x
|
node-version: 20.x
|
||||||
cache: 'pnpm'
|
cache: pnpm
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install
|
run: pnpm install
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ jobs:
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20.x
|
node-version: 20.x
|
||||||
cache: 'pnpm'
|
cache: pnpm
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --no-frozen-lockfile
|
run: pnpm install --no-frozen-lockfile
|
||||||
|
|
@ -55,7 +55,7 @@ jobs:
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20.x
|
node-version: 20.x
|
||||||
cache: 'pnpm'
|
cache: pnpm
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --no-frozen-lockfile
|
run: pnpm install --no-frozen-lockfile
|
||||||
|
|
@ -102,7 +102,7 @@ jobs:
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20.x
|
node-version: 20.x
|
||||||
cache: 'pnpm'
|
cache: pnpm
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --no-frozen-lockfile
|
run: pnpm install --no-frozen-lockfile
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ jobs:
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20.x
|
node-version: 20.x
|
||||||
cache: 'pnpm'
|
cache: pnpm
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install
|
run: pnpm install
|
||||||
|
|
@ -55,7 +55,7 @@ jobs:
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20.x
|
node-version: 20.x
|
||||||
cache: 'pnpm'
|
cache: pnpm
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install
|
run: pnpm install
|
||||||
|
|
@ -102,7 +102,7 @@ jobs:
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20.x
|
node-version: 20.x
|
||||||
cache: 'pnpm'
|
cache: pnpm
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install
|
run: pnpm install
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,6 @@ Instead of having the Bootstrap theme included locally, we recommend loading the
|
||||||
|
|
||||||
Alternatively, if you want to make modifications to the theme, check out the [theme's repo](https://github.com/gothinkster/conduit-bootstrap-template).
|
Alternatively, if you want to make modifications to the theme, check out the [theme's repo](https://github.com/gothinkster/conduit-bootstrap-template).
|
||||||
|
|
||||||
|
|
||||||
# Templates
|
# Templates
|
||||||
|
|
||||||
- [Layout](#layout)
|
- [Layout](#layout)
|
||||||
|
|
@ -48,10 +47,8 @@ Alternatively, if you want to make modifications to the theme, check out the [th
|
||||||
- [Create/Edit Article](#createedit-article)
|
- [Create/Edit Article](#createedit-article)
|
||||||
- [Article](#article)
|
- [Article](#article)
|
||||||
|
|
||||||
|
|
||||||
## Layout
|
## Layout
|
||||||
|
|
||||||
|
|
||||||
### Header
|
### Header
|
||||||
|
|
||||||
```html
|
```html
|
||||||
|
|
@ -93,7 +90,6 @@ Alternatively, if you want to make modifications to the theme, check out the [th
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Footer
|
### Footer
|
||||||
|
|
@ -319,7 +315,6 @@ Alternatively, if you want to make modifications to the theme, check out the [th
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -400,7 +395,6 @@ Alternatively, if you want to make modifications to the theme, check out the [th
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Article
|
### Article
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@
|
||||||
- [Demo](https://vue3-realworld-example-app-mutoe.vercel.app)
|
- [Demo](https://vue3-realworld-example-app-mutoe.vercel.app)
|
||||||
- [RealWorld](https://github.com/gothinkster/realworld)
|
- [RealWorld](https://github.com/gothinkster/realworld)
|
||||||
|
|
||||||
|
|
||||||
This codebase was created to demonstrate a fully fledged fullstack application built with **Vue3** including CRUD operations, authentication, routing, pagination, and more.
|
This codebase was created to demonstrate a fully fledged fullstack application built with **Vue3** including CRUD operations, authentication, routing, pagination, and more.
|
||||||
|
|
||||||
We've gone to great lengths to adhere to the **Vue3** community styleguides & best practices.
|
We've gone to great lengths to adhere to the **Vue3** community styleguides & best practices.
|
||||||
|
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
root: true,
|
|
||||||
parser: '@typescript-eslint/parser',
|
|
||||||
extends: [
|
|
||||||
'@mutoe/eslint-config-preset-ts',
|
|
||||||
'plugin:cypress/recommended',
|
|
||||||
],
|
|
||||||
env: {
|
|
||||||
'cypress/globals': true,
|
|
||||||
},
|
|
||||||
overrides: [
|
|
||||||
{
|
|
||||||
files: ['support/**/*.ts'],
|
|
||||||
rules: {
|
|
||||||
'@typescript-eslint/no-namespace': 'off',
|
|
||||||
'@typescript-eslint/consistent-type-imports': 'error',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { ROUTES } from './constant'
|
import { ROUTES } from './constant'
|
||||||
|
|
||||||
describe('Article', () => {
|
describe('article', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.intercept('GET', /articles\?limit/, { fixture: 'articles.json' })
|
cy.intercept('GET', /articles\?limit/, { fixture: 'articles.json' })
|
||||||
cy.intercept('GET', /articles\/.+/, { fixture: 'article.json' })
|
cy.intercept('GET', /articles\/.+/, { fixture: 'article.json' })
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import { ROUTES } from './constant'
|
import { ROUTES } from './constant'
|
||||||
|
|
||||||
describe('Auth', () => {
|
describe('auth', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.intercept('GET', /users/, { fixture: 'user.json' }).as('getUser')
|
cy.intercept('GET', /users/, { fixture: 'user.json' }).as('getUser')
|
||||||
cy.intercept('GET', /tags/, { fixture: 'tags.json' }).as('getTags')
|
cy.intercept('GET', /tags/, { fixture: 'tags.json' }).as('getTags')
|
||||||
cy.intercept('GET', /articles/, { fixture: 'articles.json' }).as('getArticles')
|
cy.intercept('GET', /articles/, { fixture: 'articles.json' }).as('getArticles')
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Login and logout', () => {
|
describe('login and logout', () => {
|
||||||
it('should login success when submit a valid login form', () => {
|
it('should login success when submit a valid login form', () => {
|
||||||
cy.login()
|
cy.login()
|
||||||
|
|
||||||
|
|
@ -75,7 +75,7 @@ describe('Auth', () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Register', () => {
|
describe('register', () => {
|
||||||
it('should call register API and jump to home page when submit a valid form', () => {
|
it('should call register API and jump to home page when submit a valid form', () => {
|
||||||
cy.intercept('POST', /users$/, { fixture: 'user.json' }).as('registerRequest')
|
cy.intercept('POST', /users$/, { fixture: 'user.json' }).as('registerRequest')
|
||||||
cy.visit(ROUTES.REGISTER)
|
cy.visit(ROUTES.REGISTER)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { ROUTES } from './constant'
|
import { ROUTES } from './constant'
|
||||||
|
|
||||||
describe('Favorite', () => {
|
describe('favorite', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.intercept('GET', /articles\?/, { fixture: 'articles.json' }).as('getArticles')
|
cy.intercept('GET', /articles\?/, { fixture: 'articles.json' }).as('getArticles')
|
||||||
cy.intercept('GET', /tags/, { fixture: 'tags.json' }).as('getTags')
|
cy.intercept('GET', /tags/, { fixture: 'tags.json' }).as('getTags')
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import { ROUTES } from './constant'
|
import { ROUTES } from './constant'
|
||||||
|
|
||||||
describe.only('Follow', () => {
|
describe.only('follow', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.intercept('GET', /articles\?/, { fixture: 'articles.json' }).as('getArticles')
|
cy.intercept('GET', /articles\?/, { fixture: 'articles.json' }).as('getArticles')
|
||||||
cy.intercept('GET', /tags/, { fixture: 'tags.json' }).as('getTags')
|
cy.intercept('GET', /tags/, { fixture: 'tags.json' }).as('getTags')
|
||||||
cy.intercept('GET', /profiles\/\S+/, { fixture: 'profile.json' }).as('getProfile')
|
cy.intercept('GET', /profiles\/\S+/, { fixture: 'profile.json' }).as('getProfile')
|
||||||
cy.fixture('article.json').then(article => {
|
cy.fixture('article.json').then((article) => {
|
||||||
article.article.author.username = 'foo'
|
article.article.author.username = 'foo'
|
||||||
cy.intercept('GET', /articles\/\S+/, { statusCode: 200, body: article }).as('getArticle')
|
cy.intercept('GET', /articles\/\S+/, { statusCode: 200, body: article }).as('getArticle')
|
||||||
})
|
})
|
||||||
|
|
@ -21,7 +21,7 @@ describe.only('Follow', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should call follow user api when click follow user button', () => {
|
it('should call follow user api when click follow user button', () => {
|
||||||
cy.fixture('profile.json').then(profile => {
|
cy.fixture('profile.json').then((profile) => {
|
||||||
profile.profile.following = true
|
profile.profile.following = true
|
||||||
cy.intercept('POST', /profiles\/\S+\/follow/, { statusCode: 200, body: profile }).as('followUser')
|
cy.intercept('POST', /profiles\/\S+\/follow/, { statusCode: 200, body: profile }).as('followUser')
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { ROUTES } from './constant'
|
import { ROUTES } from './constant'
|
||||||
|
|
||||||
describe('Homepage', () => {
|
describe('homepage', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.intercept('GET', /articles\?tag=butt/, { fixture: 'articles-of-tag.json' }).as('getArticlesOfTag')
|
cy.intercept('GET', /articles\?tag=butt/, { fixture: 'articles-of-tag.json' }).as('getArticlesOfTag')
|
||||||
cy.intercept('GET', /articles\?limit/, { fixture: 'articles.json' }).as('getArticles')
|
cy.intercept('GET', /articles\?limit/, { fixture: 'articles.json' }).as('getArticles')
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { ROUTES } from './constant'
|
import { ROUTES } from './constant'
|
||||||
|
|
||||||
describe('Tag', () => {
|
describe('tag', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.intercept('GET', /articles\?tag=butt/, { fixture: 'articles-of-tag.json' }).as('getArticlesOfTag')
|
cy.intercept('GET', /articles\?tag=butt/, { fixture: 'articles-of-tag.json' }).as('getArticlesOfTag')
|
||||||
cy.intercept('GET', /articles\?limit/, { fixture: 'articles.json' }).as('getArticles')
|
cy.intercept('GET', /articles\?limit/, { fixture: 'articles.json' }).as('getArticles')
|
||||||
|
|
@ -31,7 +31,7 @@ describe('Tag', () => {
|
||||||
.should('have.class', 'router-link-active')
|
.should('have.class', 'router-link-active')
|
||||||
.should('have.class', 'router-link-exact-active')
|
.should('have.class', 'router-link-exact-active')
|
||||||
|
|
||||||
cy.get('a.tag-pill.tag-default:last').invoke('text').then(tag => {
|
cy.get('a.tag-pill.tag-default:last').invoke('text').then((tag) => {
|
||||||
const path = `#/tag/${tag}`
|
const path = `#/tag/${tag}`
|
||||||
|
|
||||||
cy.url()
|
cy.url()
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,19 @@
|
||||||
{
|
{
|
||||||
"extends": "../../tsconfig.json",
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"lib": ["ESNext", "DOM"],
|
||||||
|
"baseUrl": ".",
|
||||||
|
"types": ["cypress", "@testing-library/cypress"],
|
||||||
|
"isolatedModules": false
|
||||||
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"./**/*",
|
"./**/*",
|
||||||
"../fixtures/**/*",
|
"../fixtures/**/*",
|
||||||
"../support/commands.ts",
|
"../support/commands.ts",
|
||||||
"../support/e2e.ts"
|
"../support/e2e.ts"
|
||||||
],
|
],
|
||||||
"exclude": ["../../src"],
|
"exclude": [
|
||||||
"compilerOptions": {
|
"../../src"
|
||||||
"baseUrl": ".",
|
]
|
||||||
"isolatedModules": false,
|
|
||||||
"target": "ESNext",
|
|
||||||
"lib": ["ESNext", "DOM"],
|
|
||||||
"types": ["cypress", "@testing-library/cypress"]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
/* eslint-disable ts/no-unsafe-member-access,ts/no-unsafe-assignment */
|
||||||
|
|
||||||
// ***********************************************************
|
// ***********************************************************
|
||||||
// This example support/index.js is processed and
|
// This example support/index.js is processed and
|
||||||
// loaded automatically before your test files.
|
// loaded automatically before your test files.
|
||||||
|
|
@ -27,7 +29,7 @@ declare global {
|
||||||
}
|
}
|
||||||
|
|
||||||
Cypress.Commands.add('login', (username = 'plumrx') => {
|
Cypress.Commands.add('login', (username = 'plumrx') => {
|
||||||
cy.fixture('user.json').then(authResponse => {
|
cy.fixture('user.json').then((authResponse) => {
|
||||||
authResponse.user.username = username
|
authResponse.user.username = username
|
||||||
cy.intercept('POST', /users\/login$/, { statusCode: 200, body: authResponse })
|
cy.intercept('POST', /users\/login$/, { statusCode: 200, body: authResponse })
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
import defineConfig from '@mutoe/eslint-config'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
typescript: {
|
||||||
|
tsconfigPath: [
|
||||||
|
'tsconfig.json',
|
||||||
|
'tsconfig.node.json',
|
||||||
|
'cypress/e2e/tsconfig.json',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
vue: {
|
||||||
|
sfcBlocks: {
|
||||||
|
defaultLanguage: {
|
||||||
|
script: 'ts',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
test: {
|
||||||
|
cypress: true,
|
||||||
|
},
|
||||||
|
ignores: [
|
||||||
|
'src/services/api.ts',
|
||||||
|
],
|
||||||
|
})
|
||||||
34
package.json
34
package.json
|
|
@ -2,22 +2,29 @@
|
||||||
"name": "vue3-realworld-example-app",
|
"name": "vue3-realworld-example-app",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prepare": "simple-git-hooks",
|
"prepare": "simple-git-hooks",
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"serve": "vite preview --port 4137",
|
"serve": "vite preview --port 4137",
|
||||||
"lint:script": "eslint \"{src/**/*.{ts,vue},cypress/**/*.ts}\"",
|
"type-check": "vue-tsc --noEmit",
|
||||||
"lint:tsc": "vue-tsc --noEmit",
|
"lint": "eslint --fix .",
|
||||||
"lint": "concurrently \"npm run lint:tsc\" \"npm run lint:script\"",
|
|
||||||
"test:unit": "vitest run",
|
|
||||||
"test:e2e": "npm run build && concurrently -rk -s first \"npm run serve\" \"cypress open --e2e -c baseUrl=http://localhost:4137\"",
|
|
||||||
"test:e2e:local": "cypress open --e2e -c baseUrl=http://localhost:5173",
|
|
||||||
"test:e2e:ci": "npm run build && concurrently -rk -s first \"npm run serve\" \"cypress run --e2e -c baseUrl=http://localhost:4137\"",
|
|
||||||
"test:e2e:prod": "cypress run --e2e -c baseUrl=https://vue3-realworld-example-app-mutoe.vercel.app",
|
|
||||||
"test": "npm run test:unit && npm run test:e2e:ci",
|
"test": "npm run test:unit && npm run test:e2e:ci",
|
||||||
|
"test:e2e": "npm run build && concurrently -rk -s first \"npm run serve\" \"cypress open --e2e -c baseUrl=http://localhost:4137\"",
|
||||||
|
"test:e2e:ci": "npm run build && concurrently -rk -s first \"npm run serve\" \"cypress run --e2e -c baseUrl=http://localhost:4137\"",
|
||||||
|
"test:e2e:local": "cypress open --e2e -c baseUrl=http://localhost:5173",
|
||||||
|
"test:e2e:prod": "cypress run --e2e -c baseUrl=https://vue3-realworld-example-app-mutoe.vercel.app",
|
||||||
|
"test:unit": "vitest run",
|
||||||
"generate:api": "curl -sL https://raw.githubusercontent.com/gothinkster/realworld/main/api/openapi.yml -o ./src/services/openapi.yml && sta -p ./src/services/openapi.yml -o ./src/services -n api.ts"
|
"generate:api": "curl -sL https://raw.githubusercontent.com/gothinkster/realworld/main/api/openapi.yml -o ./src/services/openapi.yml && sta -p ./src/services/openapi.yml -o ./src/services -n api.ts"
|
||||||
},
|
},
|
||||||
|
"simple-git-hooks": {
|
||||||
|
"pre-commit": "npm exec lint-staged",
|
||||||
|
"pre-push": "npm run lint && npm run build"
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"*": "eslint --fix"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"insane": "^2.6.2",
|
"insane": "^2.6.2",
|
||||||
"marked": "^11.0.0",
|
"marked": "^11.0.0",
|
||||||
|
|
@ -26,7 +33,8 @@
|
||||||
"vue-router": "^4.2.5"
|
"vue-router": "^4.2.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@mutoe/eslint-config-preset-vue": "~3.5.3",
|
"@eslint/eslintrc": "^2.1.4",
|
||||||
|
"@mutoe/eslint-config": "^2.4.5",
|
||||||
"@pinia/testing": "^0.1.3",
|
"@pinia/testing": "^0.1.3",
|
||||||
"@testing-library/cypress": "^10.0.1",
|
"@testing-library/cypress": "^10.0.1",
|
||||||
"@testing-library/user-event": "^14.5.1",
|
"@testing-library/user-event": "^14.5.1",
|
||||||
|
|
@ -48,13 +56,5 @@
|
||||||
"vitest": "^1.0.0-beta.5",
|
"vitest": "^1.0.0-beta.5",
|
||||||
"vitest-dom": "^0.1.1",
|
"vitest-dom": "^0.1.1",
|
||||||
"vue-tsc": "^1.8.22"
|
"vue-tsc": "^1.8.22"
|
||||||
},
|
|
||||||
"lint-staged": {
|
|
||||||
"src/**/*.{ts,vue,js}": "eslint --fix",
|
|
||||||
"cypress/**/*.{ts,js}": "eslint --fix"
|
|
||||||
},
|
|
||||||
"simple-git-hooks": {
|
|
||||||
"pre-commit": "npm exec lint-staged",
|
|
||||||
"pre-push": "npm run lint && npm run build"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1232
pnpm-lock.yaml
1232
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
|
@ -92,5 +92,4 @@ const allNavLinks = computed<NavLink[]>(() => [
|
||||||
const navLinks = computed(() => allNavLinks.value.filter(
|
const navLinks = computed(() => allNavLinks.value.filter(
|
||||||
l => l.display === displayStatus.value || l.display === 'all',
|
l => l.display === displayStatus.value || l.display === 'all',
|
||||||
))
|
))
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,8 @@
|
||||||
<li
|
<li
|
||||||
v-for="pageNumber in pagesCount"
|
v-for="pageNumber in pagesCount"
|
||||||
:key="pageNumber"
|
:key="pageNumber"
|
||||||
:class="['page-item', { active: isActive(pageNumber) }]"
|
class="page-item"
|
||||||
|
:class="[{ active: isActive(pageNumber) }]"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
:aria-label="`Go to page ${pageNumber}`"
|
:aria-label="`Go to page ${pageNumber}`"
|
||||||
|
|
@ -24,15 +25,14 @@ interface Props {
|
||||||
count: number
|
count: number
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Emits {
|
|
||||||
(e: 'page-change', index: number): void
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
const props = defineProps<Props>()
|
||||||
const emit = defineEmits<Emits>()
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'pageChange', index: number): void
|
||||||
|
}>()
|
||||||
|
|
||||||
const { count, page } = toRefs(props)
|
const { count, page } = toRefs(props)
|
||||||
const pagesCount = computed(() => Math.ceil(count.value / limit))
|
const pagesCount = computed(() => Math.ceil(count.value / limit))
|
||||||
const isActive = (index: number) => page.value === index
|
const isActive = (index: number) => page.value === index
|
||||||
const onPageChange = (index: number) => emit('page-change', index)
|
const onPageChange = (index: number) => emit('pageChange', index)
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ const article: Article = reactive(await api.articles.getArticle(slug).then(res =
|
||||||
|
|
||||||
const articleHandledBody = computed(() => marked(article.body))
|
const articleHandledBody = computed(() => marked(article.body))
|
||||||
|
|
||||||
const updateArticle = (newArticle: Article) => {
|
function updateArticle(newArticle: Article) {
|
||||||
Object.assign(article, newArticle)
|
Object.assign(article, newArticle)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ describe('# ArticleDetailComment', () => {
|
||||||
expect(getByRole('button', { name: 'Delete comment' })).toBeInTheDocument()
|
expect(getByRole('button', { name: 'Delete comment' })).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should emit remove comment when click remove comment button', () => {
|
it('should emit remove comment when click remove comment button', async () => {
|
||||||
const onRemoveComment = vi.fn()
|
const onRemoveComment = vi.fn()
|
||||||
const { getByRole } = render(ArticleDetailComment, renderOptions({
|
const { getByRole } = render(ArticleDetailComment, renderOptions({
|
||||||
props: {
|
props: {
|
||||||
|
|
@ -36,7 +36,7 @@ describe('# ArticleDetailComment', () => {
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
fireEvent.click(getByRole('button', { name: 'Delete comment' }))
|
await fireEvent.click(getByRole('button', { name: 'Delete comment' }))
|
||||||
|
|
||||||
expect(onRemoveComment).toHaveBeenCalled()
|
expect(onRemoveComment).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
<div class="card-footer">
|
<div class="card-footer">
|
||||||
<AppLink
|
<AppLink
|
||||||
name="profile"
|
name="profile"
|
||||||
:params="{username: comment.author.username}"
|
:params="{ username: comment.author.username }"
|
||||||
class="comment-author"
|
class="comment-author"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
|
|
@ -23,7 +23,7 @@
|
||||||
|
|
||||||
<AppLink
|
<AppLink
|
||||||
name="profile"
|
name="profile"
|
||||||
:params="{username: comment.author.username}"
|
:params="{ username: comment.author.username }"
|
||||||
class="comment-author"
|
class="comment-author"
|
||||||
>
|
>
|
||||||
{{ comment.author.username }}
|
{{ comment.author.username }}
|
||||||
|
|
@ -54,12 +54,11 @@ interface Props {
|
||||||
comment: Comment
|
comment: Comment
|
||||||
username?: string
|
username?: string
|
||||||
}
|
}
|
||||||
const props = defineProps<Props>()
|
|
||||||
|
|
||||||
interface Emits {
|
const props = defineProps<Props>()
|
||||||
|
const emit = defineEmits<{
|
||||||
(e: 'remove-comment'): boolean
|
(e: 'remove-comment'): boolean
|
||||||
}
|
}>()
|
||||||
const emit = defineEmits<Emits>()
|
|
||||||
|
|
||||||
const showRemove = computed(() => props.username !== undefined && props.username === props.comment.author.username)
|
const showRemove = computed(() => props.username !== undefined && props.username === props.comment.author.username)
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -32,15 +32,14 @@ const username = computed(() => user.value?.username)
|
||||||
|
|
||||||
const comments = ref<Comment[]>([])
|
const comments = ref<Comment[]>([])
|
||||||
|
|
||||||
const addComment = async (comment: Comment) => {
|
async function addComment(comment: Comment) {
|
||||||
comments.value.unshift(comment)
|
comments.value.unshift(comment)
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeComment = async (commentId: number) => {
|
async function removeComment(commentId: number) {
|
||||||
await api.articles.deleteArticleComment(slug, commentId)
|
await api.articles.deleteArticleComment(slug, commentId)
|
||||||
comments.value = comments.value.filter(c => c.id !== commentId)
|
comments.value = comments.value.filter(c => c.id !== commentId)
|
||||||
}
|
}
|
||||||
|
|
||||||
comments.value = await api.articles.getArticleComments(slug).then(res => res.data.comments)
|
comments.value = await api.articles.getArticleComments(slug).then(res => res.data.comments)
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -49,12 +49,11 @@ import { useUserStore } from 'src/store/user'
|
||||||
interface Props {
|
interface Props {
|
||||||
articleSlug: string
|
articleSlug: string
|
||||||
}
|
}
|
||||||
interface Emits {
|
|
||||||
(e: 'add-comment', comment: Comment): void
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
const props = defineProps<Props>()
|
||||||
const emit = defineEmits<Emits>()
|
const emit = defineEmits<{
|
||||||
|
(e: 'addComment', comment: Comment): void
|
||||||
|
}>()
|
||||||
|
|
||||||
const { user } = storeToRefs(useUserStore())
|
const { user } = storeToRefs(useUserStore())
|
||||||
|
|
||||||
|
|
@ -63,12 +62,11 @@ const { profile } = useProfile({ username })
|
||||||
|
|
||||||
const comment = ref('')
|
const comment = ref('')
|
||||||
|
|
||||||
const submitComment = async () => {
|
async function submitComment() {
|
||||||
const newComment = await api.articles
|
const newComment = await api.articles
|
||||||
.createArticleComment(props.articleSlug, { comment: { body: comment.value } })
|
.createArticleComment(props.articleSlug, { comment: { body: comment.value } })
|
||||||
.then(res => res.data.comment)
|
.then(res => res.data.comment)
|
||||||
emit('add-comment', newComment)
|
emit('addComment', newComment)
|
||||||
comment.value = ''
|
comment.value = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
<div class="article-meta">
|
<div class="article-meta">
|
||||||
<AppLink
|
<AppLink
|
||||||
name="profile"
|
name="profile"
|
||||||
:params="{username: article.author.username}"
|
:params="{ username: article.author.username }"
|
||||||
>
|
>
|
||||||
<img :src="article.author.image" :alt="article.author.username">
|
<img :src="article.author.image" :alt="article.author.username">
|
||||||
</AppLink>
|
</AppLink>
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<AppLink
|
<AppLink
|
||||||
name="profile"
|
name="profile"
|
||||||
:params="{username: article.author.username}"
|
:params="{ username: article.author.username }"
|
||||||
class="author"
|
class="author"
|
||||||
>
|
>
|
||||||
{{ article.author.username }}
|
{{ article.author.username }}
|
||||||
|
|
@ -33,7 +33,7 @@
|
||||||
<button
|
<button
|
||||||
:aria-label="article.favorited ? 'Unfavorite article' : 'Favorite article'"
|
:aria-label="article.favorited ? 'Unfavorite article' : 'Favorite article'"
|
||||||
class="btn btn-sm space"
|
class="btn btn-sm space"
|
||||||
:class="[article.favorited ? 'btn-primary':'btn-outline-primary']"
|
:class="[article.favorited ? 'btn-primary' : 'btn-outline-primary']"
|
||||||
:disabled="favoriteProcessGoing"
|
:disabled="favoriteProcessGoing"
|
||||||
@click="favoriteArticle"
|
@click="favoriteArticle"
|
||||||
>
|
>
|
||||||
|
|
@ -47,7 +47,7 @@
|
||||||
aria-label="Edit article"
|
aria-label="Edit article"
|
||||||
class="btn btn-outline-secondary btn-sm space"
|
class="btn btn-outline-secondary btn-sm space"
|
||||||
name="edit-article"
|
name="edit-article"
|
||||||
:params="{slug: article.slug}"
|
:params="{ slug: article.slug }"
|
||||||
>
|
>
|
||||||
<i class="ion-edit space" /> Edit Article
|
<i class="ion-edit space" /> Edit Article
|
||||||
</AppLink>
|
</AppLink>
|
||||||
|
|
@ -94,7 +94,7 @@ const { favoriteProcessGoing, favoriteArticle } = useFavoriteArticle({
|
||||||
onUpdate: newArticle => emit('update', newArticle),
|
onUpdate: newArticle => emit('update', newArticle),
|
||||||
})
|
})
|
||||||
|
|
||||||
const onDelete = async () => {
|
async function onDelete() {
|
||||||
await api.articles.deleteArticle(article.value.slug)
|
await api.articles.deleteArticle(article.value.slug)
|
||||||
await routerPush('global-feed')
|
await routerPush('global-feed')
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -52,5 +52,4 @@ const {
|
||||||
} = useArticles()
|
} = useArticles()
|
||||||
|
|
||||||
await fetchArticles()
|
await fetchArticles()
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,14 @@
|
||||||
<div class="article-meta">
|
<div class="article-meta">
|
||||||
<AppLink
|
<AppLink
|
||||||
name="profile"
|
name="profile"
|
||||||
:params="{username: props.article.author.username}"
|
:params="{ username: props.article.author.username }"
|
||||||
>
|
>
|
||||||
<img :src="article.author.image" :alt="props.article.author.username">
|
<img :src="article.author.image" :alt="props.article.author.username">
|
||||||
</AppLink>
|
</AppLink>
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<AppLink
|
<AppLink
|
||||||
name="profile"
|
name="profile"
|
||||||
:params="{username: props.article.author.username}"
|
:params="{ username: props.article.author.username }"
|
||||||
class="author"
|
class="author"
|
||||||
>
|
>
|
||||||
{{ article.author.username }}
|
{{ article.author.username }}
|
||||||
|
|
@ -21,9 +21,9 @@
|
||||||
<button
|
<button
|
||||||
:aria-label="article.favorited ? 'Unfavorite article' : 'Favorite article'"
|
:aria-label="article.favorited ? 'Unfavorite article' : 'Favorite article'"
|
||||||
class="btn btn-sm pull-xs-right"
|
class="btn btn-sm pull-xs-right"
|
||||||
:class="[article.favorited ? 'btn-primary':'btn-outline-primary']"
|
:class="[article.favorited ? 'btn-primary' : 'btn-outline-primary']"
|
||||||
:disabled="favoriteProcessGoing"
|
:disabled="favoriteProcessGoing"
|
||||||
@click="() =>favoriteArticle()"
|
@click="() => favoriteArticle()"
|
||||||
>
|
>
|
||||||
<i class="ion-heart" /> {{ article.favoritesCount }}
|
<i class="ion-heart" /> {{ article.favoritesCount }}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -31,7 +31,7 @@
|
||||||
|
|
||||||
<AppLink
|
<AppLink
|
||||||
name="article"
|
name="article"
|
||||||
:params="{slug: props.article.slug}"
|
:params="{ slug: props.article.slug }"
|
||||||
class="preview-link"
|
class="preview-link"
|
||||||
>
|
>
|
||||||
<h1>{{ article.title }}</h1>
|
<h1>{{ article.title }}</h1>
|
||||||
|
|
@ -73,5 +73,4 @@ const {
|
||||||
articleSlug: computed(() => props.article.slug),
|
articleSlug: computed(() => props.article.slug),
|
||||||
onUpdate: (newArticle: Article): void => emit('update', newArticle),
|
onUpdate: (newArticle: Article): void => emit('update', newArticle),
|
||||||
})
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -99,5 +99,4 @@ const show = computed<Record<ArticlesType, boolean>>(() => ({
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const links = computed<ArticlesListNavLink[]>(() => allLinks.value.filter(link => show.value[link.name]))
|
const links = computed<ArticlesListNavLink[]>(() => allLinks.value.filter(link => show.value[link.name]))
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
v-for="tag in tags"
|
v-for="tag in tags"
|
||||||
:key="tag"
|
:key="tag"
|
||||||
name="tag"
|
name="tag"
|
||||||
:params="{tag}"
|
:params="{ tag }"
|
||||||
:aria-label="tag"
|
:aria-label="tag"
|
||||||
class="tag-pill tag-default"
|
class="tag-pill tag-default"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -6,30 +6,34 @@ import { api, pageToOffset } from 'src/services'
|
||||||
import type { Article } from 'src/services/api'
|
import type { Article } from 'src/services/api'
|
||||||
import useAsync from 'src/utils/use-async'
|
import useAsync from 'src/utils/use-async'
|
||||||
|
|
||||||
export function useArticles () {
|
export function useArticles() {
|
||||||
const { articlesType, tag, username, metaChanged } = useArticlesMeta()
|
const { articlesType, tag, username, metaChanged } = useArticlesMeta()
|
||||||
|
|
||||||
const articles = ref<Article[]>([])
|
const articles = ref<Article[]>([])
|
||||||
const articlesCount = ref(0)
|
const articlesCount = ref(0)
|
||||||
const page = ref(1)
|
const page = ref(1)
|
||||||
|
|
||||||
async function fetchArticles (): Promise<void> {
|
async function fetchArticles(): Promise<void> {
|
||||||
articles.value = []
|
articles.value = []
|
||||||
let responsePromise: null | Promise<{ articles: Article[], articlesCount: number }> = null
|
let responsePromise: null | Promise<{ articles: Article[], articlesCount: number }> = null
|
||||||
|
|
||||||
if (articlesType.value === 'my-feed') {
|
if (articlesType.value === 'my-feed') {
|
||||||
responsePromise = api.articles.getArticlesFeed(pageToOffset(page.value))
|
responsePromise = api.articles.getArticlesFeed(pageToOffset(page.value))
|
||||||
.then(res => res.data)
|
.then(res => res.data)
|
||||||
} else if (articlesType.value === 'tag-feed' && tag.value) {
|
}
|
||||||
|
else if (articlesType.value === 'tag-feed' && tag.value) {
|
||||||
responsePromise = api.articles.getArticles({ tag: tag.value, ...pageToOffset(page.value) })
|
responsePromise = api.articles.getArticles({ tag: tag.value, ...pageToOffset(page.value) })
|
||||||
.then(res => res.data)
|
.then(res => res.data)
|
||||||
} else if (articlesType.value === 'user-feed' && username.value) {
|
}
|
||||||
|
else if (articlesType.value === 'user-feed' && username.value) {
|
||||||
responsePromise = api.articles.getArticles({ author: username.value, ...pageToOffset(page.value) })
|
responsePromise = api.articles.getArticles({ author: username.value, ...pageToOffset(page.value) })
|
||||||
.then(res => res.data)
|
.then(res => res.data)
|
||||||
} else if (articlesType.value === 'user-favorites-feed' && username.value) {
|
}
|
||||||
|
else if (articlesType.value === 'user-favorites-feed' && username.value) {
|
||||||
responsePromise = api.articles.getArticles({ favorited: username.value, ...pageToOffset(page.value) })
|
responsePromise = api.articles.getArticles({ favorited: username.value, ...pageToOffset(page.value) })
|
||||||
.then(res => res.data)
|
.then(res => res.data)
|
||||||
} else if (articlesType.value === 'global-feed') {
|
}
|
||||||
|
else if (articlesType.value === 'global-feed') {
|
||||||
responsePromise = api.articles.getArticles(pageToOffset(page.value))
|
responsePromise = api.articles.getArticles(pageToOffset(page.value))
|
||||||
.then(res => res.data)
|
.then(res => res.data)
|
||||||
}
|
}
|
||||||
|
|
@ -55,11 +59,10 @@ export function useArticles () {
|
||||||
const { active: articlesDownloading, run: runWrappedFetchArticles } = useAsync(fetchArticles)
|
const { active: articlesDownloading, run: runWrappedFetchArticles } = useAsync(fetchArticles)
|
||||||
|
|
||||||
watch(metaChanged, async () => {
|
watch(metaChanged, async () => {
|
||||||
if (page.value === 1) {
|
if (page.value === 1)
|
||||||
await runWrappedFetchArticles()
|
await runWrappedFetchArticles()
|
||||||
} else {
|
else
|
||||||
changePage(1)
|
changePage(1)
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(page, runWrappedFetchArticles)
|
watch(page, runWrappedFetchArticles)
|
||||||
|
|
@ -81,7 +84,7 @@ export type ArticlesType = 'global-feed' | 'my-feed' | 'tag-feed' | 'user-feed'
|
||||||
|
|
||||||
export const articlesTypes: ArticlesType[] = ['global-feed', 'my-feed', 'tag-feed', 'user-feed', 'user-favorites-feed']
|
export const articlesTypes: ArticlesType[] = ['global-feed', 'my-feed', 'tag-feed', 'user-feed', 'user-favorites-feed']
|
||||||
|
|
||||||
export const isArticlesType = (type: any): type is ArticlesType => articlesTypes.includes(type)
|
export const isArticlesType = (type: unknown): type is ArticlesType => articlesTypes.includes(type as ArticlesType)
|
||||||
|
|
||||||
const routeNameToArticlesType: Partial<Record<AppRouteNames, ArticlesType>> = {
|
const routeNameToArticlesType: Partial<Record<AppRouteNames, ArticlesType>> = {
|
||||||
'global-feed': 'global-feed',
|
'global-feed': 'global-feed',
|
||||||
|
|
@ -97,7 +100,7 @@ interface UseArticlesMetaReturn {
|
||||||
articlesType: ComputedRef<ArticlesType>
|
articlesType: ComputedRef<ArticlesType>
|
||||||
metaChanged: ComputedRef<string>
|
metaChanged: ComputedRef<string>
|
||||||
}
|
}
|
||||||
function useArticlesMeta (): UseArticlesMetaReturn {
|
function useArticlesMeta(): UseArticlesMetaReturn {
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
const tag = ref('')
|
const tag = ref('')
|
||||||
|
|
@ -106,9 +109,10 @@ function useArticlesMeta (): UseArticlesMetaReturn {
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => route.name,
|
() => route.name,
|
||||||
routeName => {
|
(routeName) => {
|
||||||
const possibleArticlesType = routeNameToArticlesType[routeName as AppRouteNames]
|
const possibleArticlesType = routeNameToArticlesType[routeName as AppRouteNames]
|
||||||
if (!isArticlesType(possibleArticlesType)) return
|
if (!isArticlesType(possibleArticlesType))
|
||||||
|
return
|
||||||
|
|
||||||
articlesType.value = possibleArticlesType
|
articlesType.value = possibleArticlesType
|
||||||
},
|
},
|
||||||
|
|
@ -117,20 +121,18 @@ function useArticlesMeta (): UseArticlesMetaReturn {
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => route.params.username,
|
() => route.params.username,
|
||||||
usernameParam => {
|
(usernameParam) => {
|
||||||
if (usernameParam !== username.value) {
|
if (usernameParam !== username.value)
|
||||||
username.value = typeof usernameParam === 'string' ? usernameParam : ''
|
username.value = typeof usernameParam === 'string' ? usernameParam : ''
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => route.params.tag,
|
() => route.params.tag,
|
||||||
tagParam => {
|
(tagParam) => {
|
||||||
if (tagParam !== tag.value) {
|
if (tagParam !== tag.value)
|
||||||
tag.value = typeof tagParam === 'string' ? tagParam : ''
|
tag.value = typeof tagParam === 'string' ? tagParam : ''
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ interface useFavoriteArticleProps {
|
||||||
onUpdate: (newArticle: Article) => void
|
onUpdate: (newArticle: Article) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useFavoriteArticle = ({ isFavorited, articleSlug, onUpdate }: useFavoriteArticleProps) => {
|
export function useFavoriteArticle({ isFavorited, articleSlug, onUpdate }: useFavoriteArticleProps) {
|
||||||
const favoriteArticle = async () => {
|
const favoriteArticle = async () => {
|
||||||
const requestor = isFavorited.value ? api.articles.deleteArticleFavorite : api.articles.createArticleFavorite
|
const requestor = isFavorited.value ? api.articles.deleteArticleFavorite : api.articles.createArticleFavorite
|
||||||
const article = await requestor(articleSlug.value).then(res => res.data.article)
|
const article = await requestor(articleSlug.value).then(res => res.data.article)
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,8 @@ interface UseFollowProps {
|
||||||
onUpdate: (profile: Profile) => void
|
onUpdate: (profile: Profile) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useFollow ({ username, following, onUpdate }: UseFollowProps) {
|
export function useFollow({ username, following, onUpdate }: UseFollowProps) {
|
||||||
async function toggleFollow () {
|
async function toggleFollow() {
|
||||||
const requester = following.value ? api.profiles.unfollowUserByUsername : api.profiles.followUserByUsername
|
const requester = following.value ? api.profiles.unfollowUserByUsername : api.profiles.followUserByUsername
|
||||||
const profile = await requester(username.value).then(res => res.data.profile)
|
const profile = await requester(username.value).then(res => res.data.profile)
|
||||||
onUpdate(profile)
|
onUpdate(profile)
|
||||||
|
|
|
||||||
|
|
@ -7,17 +7,19 @@ interface UseProfileProps {
|
||||||
username: ComputedRef<string>
|
username: ComputedRef<string>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useProfile ({ username }: UseProfileProps) {
|
export function useProfile({ username }: UseProfileProps) {
|
||||||
const profile = ref<Profile | null>(null)
|
const profile = ref<Profile | null>(null)
|
||||||
|
|
||||||
async function fetchProfile (): Promise<void> {
|
async function fetchProfile(): Promise<void> {
|
||||||
updateProfile(null)
|
updateProfile(null)
|
||||||
if (!username.value) return
|
if (!username.value)
|
||||||
|
return
|
||||||
|
|
||||||
const profileData = await api.profiles.getProfileByUsername(username.value).then(res => res.data.profile)
|
const profileData = await api.profiles.getProfileByUsername(username.value).then(res => res.data.profile)
|
||||||
updateProfile(profileData)
|
updateProfile(profileData)
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateProfile (profileData: Profile | null): void {
|
function updateProfile(profileData: Profile | null): void {
|
||||||
profile.value = profileData
|
profile.value = profileData
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { api } from 'src/services'
|
import { api } from 'src/services'
|
||||||
|
|
||||||
export function useTags () {
|
export function useTags() {
|
||||||
const tags = ref<string[]>([])
|
const tags = ref<string[]>([])
|
||||||
|
|
||||||
async function fetchTags (): Promise<void> {
|
async function fetchTags(): Promise<void> {
|
||||||
tags.value = []
|
tags.value = []
|
||||||
tags.value = await api.tags.getTags().then(({ data }) => data.tags)
|
tags.value = await api.tags.getTags().then(({ data }) => data.tags)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
export const CONFIG = {
|
export const CONFIG = {
|
||||||
API_HOST: import.meta.env.VITE_API_HOST || '',
|
API_HOST: String(import.meta.env.VITE_API_HOST) || '',
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -98,15 +98,15 @@ const form: FormState = reactive({
|
||||||
})
|
})
|
||||||
|
|
||||||
const newTag = ref<string>('')
|
const newTag = ref<string>('')
|
||||||
const addTag = () => {
|
function addTag() {
|
||||||
form.tagList.push(newTag.value.trim())
|
form.tagList.push(newTag.value.trim())
|
||||||
newTag.value = ''
|
newTag.value = ''
|
||||||
}
|
}
|
||||||
const removeTag = (tag: string) => {
|
function removeTag(tag: string) {
|
||||||
form.tagList = form.tagList.filter(t => t !== tag)
|
form.tagList = form.tagList.filter(t => t !== tag)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchArticle (slug: string) {
|
async function fetchArticle(slug: string) {
|
||||||
const article = await api.articles.getArticle(slug).then(res => res.data.article)
|
const article = await api.articles.getArticle(slug).then(res => res.data.article)
|
||||||
|
|
||||||
// FIXME: I always feel a little wordy here
|
// FIXME: I always feel a little wordy here
|
||||||
|
|
@ -116,18 +116,18 @@ async function fetchArticle (slug: string) {
|
||||||
form.tagList = article.tagList
|
form.tagList = article.tagList
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(async () => {
|
||||||
if (slug.value) fetchArticle(slug.value)
|
if (slug.value)
|
||||||
|
await fetchArticle(slug.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
const onSubmit = async () => {
|
async function onSubmit() {
|
||||||
let article: Article
|
let article: Article
|
||||||
if (slug.value) {
|
if (slug.value)
|
||||||
article = await api.articles.updateArticle(slug.value, { article: form }).then(res => res.data.article)
|
article = await api.articles.updateArticle(slug.value, { article: form }).then(res => res.data.article)
|
||||||
} else {
|
else
|
||||||
article = await api.articles.createArticle({ article: form }).then(res => res.data.article)
|
article = await api.articles.createArticle({ article: form }).then(res => res.data.article)
|
||||||
}
|
|
||||||
return router.push({ name: 'article', params: { slug: article.slug } })
|
return router.push({ name: 'article', params: { slug: article.slug } })
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ describe('# Login page', () => {
|
||||||
|
|
||||||
it('should not trigger api call when user submit a invalid form', async () => {
|
it('should not trigger api call when user submit a invalid form', async () => {
|
||||||
const { getByRole, getByPlaceholderText } = render(Login, renderOptions())
|
const { getByRole, getByPlaceholderText } = render(Login, renderOptions())
|
||||||
const formElement = getByRole('form', { name: 'Login form' }) as HTMLFormElement
|
const formElement = getByRole<HTMLFormElement>('form', { name: 'Login form' })
|
||||||
vi.spyOn(formElement, 'checkValidity')
|
vi.spyOn(formElement, 'checkValidity')
|
||||||
|
|
||||||
expect(getByRole('button', { name: 'Sign in' })).toHaveProperty('disabled', true)
|
expect(getByRole('button', { name: 'Sign in' })).toHaveProperty('disabled', true)
|
||||||
|
|
|
||||||
|
|
@ -80,16 +80,18 @@ const { updateUser } = useUserStore()
|
||||||
|
|
||||||
const errors = ref()
|
const errors = ref()
|
||||||
|
|
||||||
const login = async () => {
|
async function login() {
|
||||||
errors.value = {}
|
errors.value = {}
|
||||||
|
|
||||||
if (!formRef.value?.checkValidity()) return
|
if (!formRef.value?.checkValidity())
|
||||||
|
return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await api.users.login({ user: form })
|
const result = await api.users.login({ user: form })
|
||||||
updateUser(result.data.user)
|
updateUser(result.data.user)
|
||||||
await routerPush('global-feed')
|
await routerPush('global-feed')
|
||||||
} catch (error) {
|
}
|
||||||
|
catch (error) {
|
||||||
if (isFetchError(error)) {
|
if (isFetchError(error)) {
|
||||||
errors.value = error.error?.errors
|
errors.value = error.error?.errors
|
||||||
return
|
return
|
||||||
|
|
@ -97,5 +99,4 @@ const login = async () => {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,6 @@ const { user, isAuthorized } = storeToRefs(useUserStore())
|
||||||
|
|
||||||
const showEdit = computed<boolean>(() => isAuthorized && user.value?.username === username.value)
|
const showEdit = computed<boolean>(() => isAuthorized && user.value?.username === username.value)
|
||||||
const showFollow = computed<boolean>(() => user.value?.username !== username.value)
|
const showFollow = computed<boolean>(() => user.value?.username !== username.value)
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ describe('# Register form', () => {
|
||||||
|
|
||||||
it('should not trigger api call when user submit a invalid form', async () => {
|
it('should not trigger api call when user submit a invalid form', async () => {
|
||||||
const { getByRole, getByPlaceholderText } = render(Register, renderOptions())
|
const { getByRole, getByPlaceholderText } = render(Register, renderOptions())
|
||||||
const formElement = getByRole('form', { name: 'Registration form' }) as HTMLFormElement
|
const formElement = getByRole<HTMLFormElement>('form', { name: 'Registration form' })
|
||||||
vi.spyOn(formElement, 'checkValidity')
|
vi.spyOn(formElement, 'checkValidity')
|
||||||
|
|
||||||
expect(getByRole('button', { name: 'Sign up' })).toHaveProperty('disabled', true)
|
expect(getByRole('button', { name: 'Sign up' })).toHaveProperty('disabled', true)
|
||||||
|
|
|
||||||
|
|
@ -89,19 +89,20 @@ const { updateUser } = useUserStore()
|
||||||
|
|
||||||
const errors = ref()
|
const errors = ref()
|
||||||
|
|
||||||
const register = async () => {
|
async function register() {
|
||||||
errors.value = {}
|
errors.value = {}
|
||||||
|
|
||||||
if (!formRef.value?.checkValidity()) return
|
if (!formRef.value?.checkValidity())
|
||||||
|
return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await api.users.createUser({ user: form })
|
const result = await api.users.createUser({ user: form })
|
||||||
updateUser(result.data.user)
|
updateUser(result.data.user)
|
||||||
await routerPush('global-feed')
|
await routerPush('global-feed')
|
||||||
} catch (error) {
|
}
|
||||||
if (isFetchError(error)) {
|
catch (error) {
|
||||||
|
if (isFetchError(error))
|
||||||
errors.value = error.error?.errors
|
errors.value = error.error?.errors
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -97,29 +97,30 @@ const form: UpdateUser = reactive({})
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const errors = ref()
|
const errors = ref()
|
||||||
|
|
||||||
const onSubmit = async () => {
|
async function onSubmit() {
|
||||||
errors.value = {}
|
errors.value = {}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// eslint-disable-next-line unicorn/no-array-reduce
|
// eslint-disable-next-line unicorn/no-array-reduce, ts/no-unsafe-assignment
|
||||||
const filteredForm = Object.entries(form).reduce((form, [k, v]) => v === null ? form : Object.assign(form, { [k]: v }), {})
|
const filteredForm = Object.entries(form).reduce((form, [k, v]) => v === null ? form : Object.assign(form, { [k]: v }), {})
|
||||||
const userData = await api.user.updateCurrentUser({ user: filteredForm }).then(res => res.data.user)
|
const userData = await api.user.updateCurrentUser({ user: filteredForm }).then(res => res.data.user)
|
||||||
userStore.updateUser(userData)
|
userStore.updateUser(userData)
|
||||||
await routerPush('profile', { username: userData.username })
|
await routerPush('profile', { username: userData.username })
|
||||||
} catch (error) {
|
}
|
||||||
if (isFetchError(error)) {
|
catch (error) {
|
||||||
|
if (isFetchError(error))
|
||||||
errors.value = error.error?.errors
|
errors.value = error.error?.errors
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onLogout = async () => {
|
async function onLogout() {
|
||||||
userStore.updateUser(null)
|
userStore.updateUser(null)
|
||||||
await routerPush('global-feed')
|
await routerPush('global-feed')
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (!userStore.isAuthorized) return await routerPush('login')
|
if (!userStore.isAuthorized)
|
||||||
|
return await routerPush('login')
|
||||||
|
|
||||||
form.image = userStore.user?.image
|
form.image = userStore.user?.image
|
||||||
form.username = userStore.user?.username
|
form.username = userStore.user?.username
|
||||||
|
|
@ -134,5 +135,4 @@ const isButtonDisabled = computed(() =>
|
||||||
&& form.email === userStore.user?.email
|
&& form.email === userStore.user?.email
|
||||||
&& !form.password,
|
&& !form.password,
|
||||||
)
|
)
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import type { App } from 'vue'
|
import type { App } from 'vue'
|
||||||
import AppLink from 'src/components/AppLink.vue'
|
import AppLink from 'src/components/AppLink.vue'
|
||||||
|
|
||||||
export default function registerGlobalComponents (app: App): void {
|
export default function registerGlobalComponents(app: App): void {
|
||||||
app.component('AppLink', AppLink)
|
app.component('AppLink', AppLink)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
/* eslint-disable array-element-newline */
|
/* eslint-disable antfu/consistent-list-newline */
|
||||||
import insane from 'insane'
|
import insane from 'insane'
|
||||||
import { marked } from 'marked'
|
import { marked } from 'marked'
|
||||||
|
|
||||||
export default (markdown?: string): string => {
|
export default (markdown?: string): string => {
|
||||||
if (!markdown) return ''
|
if (!markdown)
|
||||||
|
return ''
|
||||||
const html = marked(markdown)
|
const html = marked(markdown)
|
||||||
|
|
||||||
|
// eslint-disable-next-line ts/no-unsafe-return,ts/no-unsafe-call
|
||||||
return insane(html, {
|
return insane(html, {
|
||||||
allowedTags: ['a', 'article', 'b', 'blockquote', 'br', 'caption', 'code', 'del', 'details', 'div', 'em',
|
allowedTags: ['a', 'article', 'b', 'blockquote', 'br', 'caption', 'code', 'del', 'details', 'div', 'em',
|
||||||
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'img', 'input', 'ins', 'kbd', 'li', 'main',
|
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'img', 'input', 'ins', 'kbd', 'li', 'main',
|
||||||
|
|
@ -28,11 +30,11 @@ export default (markdown?: string): string => {
|
||||||
td: ['align'],
|
td: ['align'],
|
||||||
input: ['disabled', 'type', 'checked'],
|
input: ['disabled', 'type', 'checked'],
|
||||||
},
|
},
|
||||||
filter: ({ tag, attrs }: {tag: string, attrs: Record<string, string>}) => {
|
filter: ({ tag, attrs }: { tag: string, attrs: Record<string, string> }) => {
|
||||||
// Display checklist
|
// Display checklist
|
||||||
if (tag === 'input') {
|
if (tag === 'input')
|
||||||
return attrs.type === 'checkbox' && attrs.disabled === ''
|
return attrs.type === 'checkbox' && attrs.disabled === ''
|
||||||
}
|
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -3,5 +3,6 @@ import { userStorage } from 'src/store/user'
|
||||||
|
|
||||||
export default function (): void {
|
export default function (): void {
|
||||||
const token = userStorage.get()?.token
|
const token = userStorage.get()?.token
|
||||||
if (token !== undefined) api.setSecurityData(token)
|
if (token !== undefined)
|
||||||
|
api.setSecurityData(token)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@ export const router = createRouter({
|
||||||
routes,
|
routes,
|
||||||
})
|
})
|
||||||
|
|
||||||
export function routerPush (name: AppRouteNames, params?: RouteParams): ReturnType<typeof router.push> {
|
export function routerPush(name: AppRouteNames, params?: RouteParams): ReturnType<typeof router.push> {
|
||||||
return params === undefined
|
return params === undefined
|
||||||
? router.push({ name })
|
? router.push({ name })
|
||||||
: router.push({ name, params })
|
: router.push({ name, params })
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ export const limit = 10
|
||||||
|
|
||||||
export const api = new Api({
|
export const api = new Api({
|
||||||
baseUrl: `${CONFIG.API_HOST}/api`,
|
baseUrl: `${CONFIG.API_HOST}/api`,
|
||||||
securityWorker: token => token ? { headers: { Authorization: `Bearer ${token}` } } : {},
|
securityWorker: token => token ? { headers: { Authorization: `Bearer ${String(token)}` } } : {},
|
||||||
baseApiParams: {
|
baseApiParams: {
|
||||||
headers: {
|
headers: {
|
||||||
'content-type': ContentType.Json,
|
'content-type': ContentType.Json,
|
||||||
|
|
@ -15,11 +15,11 @@ export const api = new Api({
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
export function pageToOffset (page: number = 1, localLimit = limit): {limit: number, offset: number} {
|
export function pageToOffset(page: number = 1, localLimit = limit): { limit: number, offset: number } {
|
||||||
const offset = (page - 1) * localLimit
|
const offset = (page - 1) * localLimit
|
||||||
return { limit: localLimit, offset }
|
return { limit: localLimit, offset }
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isFetchError<E = GenericErrorModel> (e: unknown): e is HttpResponse<unknown, E> {
|
export function isFetchError<E = GenericErrorModel>(e: unknown): e is HttpResponse<unknown, E> {
|
||||||
return e instanceof Object && 'error' in e
|
return e instanceof Object && 'error' in e
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
/* eslint-disable @typescript-eslint/triple-slash-reference */
|
|
||||||
/// <reference types="vitest-dom/extend-expect" />
|
/// <reference types="vitest-dom/extend-expect" />
|
||||||
import 'vitest-dom/extend-expect'
|
import 'vitest-dom/extend-expect'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,12 +12,13 @@ export const useUserStore = defineStore('user', () => {
|
||||||
const user = ref(userStorage.get())
|
const user = ref(userStorage.get())
|
||||||
const isAuthorized = computed(() => !!user.value)
|
const isAuthorized = computed(() => !!user.value)
|
||||||
|
|
||||||
function updateUser (userData?: User | null) {
|
function updateUser(userData?: User | null) {
|
||||||
if (userData) {
|
if (userData) {
|
||||||
userStorage.set(userData)
|
userStorage.set(userData)
|
||||||
api.setSecurityData(userData.token)
|
api.setSecurityData(userData.token)
|
||||||
user.value = userData
|
user.value = userData
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
userStorage.remove()
|
userStorage.remove()
|
||||||
api.setSecurityData(null)
|
api.setSecurityData(null)
|
||||||
user.value = null
|
user.value = null
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,6 @@
|
||||||
/* eslint-disable @typescript-eslint/consistent-type-imports */
|
|
||||||
|
|
||||||
import type AppLink from 'src/components/AppLink.vue'
|
import type AppLink from 'src/components/AppLink.vue'
|
||||||
|
|
||||||
declare module '@vue/runtime-core' {
|
declare module '@vue/runtime-core' {
|
||||||
// noinspection JSUnusedGlobalSymbols
|
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
export const dateFilter = (dateString: string): string => {
|
export function dateFilter(dateString: string): string {
|
||||||
const date = new Date(dateString)
|
const date = new Date(dateString)
|
||||||
return date.toLocaleDateString('en-US', {
|
return date.toLocaleDateString('en-US', {
|
||||||
month: 'long',
|
month: 'long',
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
export default function params2query (params: Record<string, string | number | boolean>): string {
|
export default function params2query(params: Record<string, string | number | boolean>): string {
|
||||||
return Object.entries(params).map(([key, value]) => `${key}=${value.toString()}`).join('&')
|
return Object.entries(params).map(([key, value]) => `${key}=${value.toString()}`).join('&')
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,26 +4,27 @@ export default class Storage<T = unknown> {
|
||||||
private readonly key: string
|
private readonly key: string
|
||||||
private readonly storageType: StorageType
|
private readonly storageType: StorageType
|
||||||
|
|
||||||
constructor (key: string, storageType: StorageType = 'localStorage') {
|
constructor(key: string, storageType: StorageType = 'localStorage') {
|
||||||
this.key = key
|
this.key = key
|
||||||
this.storageType = storageType
|
this.storageType = storageType
|
||||||
}
|
}
|
||||||
|
|
||||||
get (): T | null {
|
get(): T | null {
|
||||||
try {
|
try {
|
||||||
const value = window[this.storageType].getItem(this.key) ?? ''
|
const value = window[this.storageType].getItem(this.key) ?? ''
|
||||||
return JSON.parse(value)
|
return JSON.parse(value) as T
|
||||||
} catch {
|
}
|
||||||
|
catch {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
set (value: T): void {
|
set(value: T): void {
|
||||||
const strValue = JSON.stringify(value)
|
const strValue = JSON.stringify(value)
|
||||||
window[this.storageType].setItem(this.key, strValue)
|
window[this.storageType].setItem(this.key, strValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
remove (): void {
|
remove(): void {
|
||||||
window[this.storageType].removeItem(this.key)
|
window[this.storageType].removeItem(this.key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,12 @@ import { afterAll, afterEach, beforeAll } from 'vitest'
|
||||||
import AppLink from 'src/components/AppLink.vue'
|
import AppLink from 'src/components/AppLink.vue'
|
||||||
import { routes } from 'src/router'
|
import { routes } from 'src/router'
|
||||||
|
|
||||||
export const createTestRouter = (base?: string): Router => createRouter({
|
export function createTestRouter(base?: string): Router {
|
||||||
routes,
|
return createRouter({
|
||||||
history: createMemoryHistory(base),
|
routes,
|
||||||
})
|
history: createMemoryHistory(base),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
interface RenderOptionsArgs {
|
interface RenderOptionsArgs {
|
||||||
props: Record<string, unknown>
|
props: Record<string, unknown>
|
||||||
|
|
@ -28,16 +30,16 @@ interface RenderOptionsArgs {
|
||||||
|
|
||||||
const scheduler = typeof setImmediate === 'function' ? setImmediate : setTimeout
|
const scheduler = typeof setImmediate === 'function' ? setImmediate : setTimeout
|
||||||
|
|
||||||
export function flushPromises (): Promise<void> {
|
export function flushPromises(): Promise<void> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
scheduler(resolve, 0)
|
scheduler(resolve, 0)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderOptions (): RenderOptions
|
export function renderOptions(): RenderOptions
|
||||||
export function renderOptions (args: Partial<Omit<RenderOptionsArgs, 'initialRoute'>>): RenderOptions
|
export function renderOptions(args: Partial<Omit<RenderOptionsArgs, 'initialRoute'>>): RenderOptions
|
||||||
export async function renderOptions (args: (Partial<RenderOptionsArgs> & {initialRoute: RouteLocationRaw})): Promise<RenderOptions>
|
export async function renderOptions(args: (Partial<RenderOptionsArgs> & { initialRoute: RouteLocationRaw })): Promise<RenderOptions>
|
||||||
export function renderOptions (args: Partial<RenderOptionsArgs> = {}): RenderOptions | Promise<RenderOptions> {
|
export function renderOptions(args: Partial<RenderOptionsArgs> = {}): RenderOptions | Promise<RenderOptions> {
|
||||||
const router = args.router || createTestRouter()
|
const router = args.router || createTestRouter()
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
|
|
@ -52,28 +54,31 @@ export function renderOptions (args: Partial<RenderOptionsArgs> = {}): RenderOpt
|
||||||
...args.initialState,
|
...args.initialState,
|
||||||
},
|
},
|
||||||
stubActions: args.stubActions ?? false,
|
stubActions: args.stubActions ?? false,
|
||||||
})],
|
}),
|
||||||
|
],
|
||||||
components: { AppLink },
|
components: { AppLink },
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const { initialRoute } = args
|
const { initialRoute } = args
|
||||||
|
|
||||||
if (!initialRoute) return result
|
if (!initialRoute)
|
||||||
|
return result
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
router.replace(initialRoute).then(() => resolve(result))
|
void router.replace(initialRoute).then(() => resolve(result))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function asyncWrapper (component: ReturnType<typeof defineComponent>, props?: Record<string, unknown>): ReturnType<typeof defineComponent> {
|
export function asyncWrapper(component: ReturnType<typeof defineComponent>, props?: Record<string, unknown>): ReturnType<typeof defineComponent> {
|
||||||
return defineComponent({
|
return defineComponent({
|
||||||
render () {
|
render() {
|
||||||
return h(
|
return h(
|
||||||
'div',
|
'div',
|
||||||
{ id: 'root' },
|
{ id: 'root' },
|
||||||
h(Suspense, null, {
|
h(Suspense, null, {
|
||||||
default () {
|
default() {
|
||||||
|
// eslint-disable-next-line ts/no-unsafe-argument
|
||||||
return h(component, props)
|
return h(component, props)
|
||||||
},
|
},
|
||||||
fallback: h('div', 'Loading...'),
|
fallback: h('div', 'Loading...'),
|
||||||
|
|
@ -83,7 +88,7 @@ export function asyncWrapper (component: ReturnType<typeof defineComponent>, pro
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function waitForServerRequest (server: SetupServer, method: string, url: string, flush = true): Promise<Request> {
|
async function waitForServerRequest(server: SetupServer, method: string, url: string, flush = true): Promise<Request> {
|
||||||
let expectedRequestId = ''
|
let expectedRequestId = ''
|
||||||
let expectedRequest: Request
|
let expectedRequest: Request
|
||||||
|
|
||||||
|
|
@ -98,11 +103,13 @@ async function waitForServerRequest (server: SetupServer, method: string, url: s
|
||||||
})
|
})
|
||||||
|
|
||||||
server.events.on('response:mocked', ({ requestId: reqId }) => {
|
server.events.on('response:mocked', ({ requestId: reqId }) => {
|
||||||
if (reqId === expectedRequestId) resolve(expectedRequest)
|
if (reqId === expectedRequestId)
|
||||||
|
resolve(expectedRequest)
|
||||||
})
|
})
|
||||||
|
|
||||||
server.events.on('request:unhandled', ({ request: req, requestId: reqId }) => {
|
server.events.on('request:unhandled', ({ request: req, requestId: reqId }) => {
|
||||||
if (reqId === expectedRequestId) reject(new Error(`The ${req.method} ${req.url} request was unhandled.`))
|
if (reqId === expectedRequestId)
|
||||||
|
reject(new Error(`The ${req.method} ${req.url} request was unhandled.`))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
flush && await flushPromises()
|
flush && await flushPromises()
|
||||||
|
|
@ -139,16 +146,20 @@ type Listener =
|
||||||
* })
|
* })
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export function setupMockServer (...listeners: Listener[]) {
|
export function setupMockServer(...listeners: Listener[]) {
|
||||||
const parseArgs = (args: Listener): [string, string, number, (object | null)] => {
|
const parseArgs = (args: Listener): [string, string, number, (object | null)] => {
|
||||||
if (args.length === 4) return args
|
if (args.length === 4)
|
||||||
|
return args
|
||||||
if (args.length === 3) {
|
if (args.length === 3) {
|
||||||
if (typeof args[1] === 'number') return ['all', args[0], args[1], args[2] as object] // ['all', path, 200, object]
|
if (typeof args[1] === 'number')
|
||||||
if (typeof args[2] === 'number') return [args[0], args[1], args[2], null] // [method, path, status, null]
|
return ['all', args[0], args[1], args[2] as object] // ['all', path, 200, object]
|
||||||
|
if (typeof args[2] === 'number')
|
||||||
|
return [args[0], args[1], args[2], null] // [method, path, status, null]
|
||||||
return [args[0], args[1], 200, args[2]] // [method, path, 200, object]
|
return [args[0], args[1], 200, args[2]] // [method, path, 200, object]
|
||||||
}
|
}
|
||||||
if (args.length === 2) {
|
if (args.length === 2) {
|
||||||
if (typeof args[1] === 'string') return [args[0], args[1], 200, null]
|
if (typeof args[1] === 'string')
|
||||||
|
return [args[0], args[1], 200, null]
|
||||||
return ['all', args[0], 200, args[1]]
|
return ['all', args[0], 200, args[1]]
|
||||||
}
|
}
|
||||||
return ['all', args[0], 200, null]
|
return ['all', args[0], 200, null]
|
||||||
|
|
@ -168,11 +179,11 @@ export function setupMockServer (...listeners: Listener[]) {
|
||||||
afterEach(() => void server.resetHandlers())
|
afterEach(() => void server.resetHandlers())
|
||||||
afterAll(() => void server.close())
|
afterAll(() => void server.close())
|
||||||
|
|
||||||
async function waitForRequest (path: string): Promise<Request>
|
async function waitForRequest(path: string): Promise<Request>
|
||||||
async function waitForRequest (path: string, flush: boolean): Promise<Request>
|
async function waitForRequest(path: string, flush: boolean): Promise<Request>
|
||||||
async function waitForRequest (method: HttpMethod, path: string): Promise<Request>
|
async function waitForRequest(method: HttpMethod, path: string): Promise<Request>
|
||||||
async function waitForRequest (method: HttpMethod, path: string, flush: boolean): Promise<Request>
|
async function waitForRequest(method: HttpMethod, path: string, flush: boolean): Promise<Request>
|
||||||
async function waitForRequest (...args: [string] | [string, boolean] | [HttpMethod, string] | [HttpMethod, string, boolean]): Promise<Request> {
|
async function waitForRequest(...args: [string] | [string, boolean] | [HttpMethod, string] | [HttpMethod, string, boolean]): Promise<Request> {
|
||||||
const [method, path, flush] = args.length === 1
|
const [method, path, flush] = args.length === 1
|
||||||
? ['all', args[0]] // ['all', path]
|
? ['all', args[0]] // ['all', path]
|
||||||
: args.length === 2 && typeof args[1] === 'boolean'
|
: args.length === 2 && typeof args[1] === 'boolean'
|
||||||
|
|
@ -185,7 +196,7 @@ export function setupMockServer (...listeners: Listener[]) {
|
||||||
|
|
||||||
const originalUse = server.use.bind(server)
|
const originalUse = server.use.bind(server)
|
||||||
|
|
||||||
function use (...listeners: Listener[]) {
|
function use(...listeners: Listener[]) {
|
||||||
originalUse(
|
originalUse(
|
||||||
...listeners.map((args) => {
|
...listeners.map((args) => {
|
||||||
let [method, path, status, response] = parseArgs(args)
|
let [method, path, status, response] = parseArgs(args)
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ interface UseAsync<T extends (...args: unknown[]) => unknown> {
|
||||||
run: (...args: Parameters<T>) => Promise<ReturnType<T>>
|
run: (...args: Parameters<T>) => Promise<ReturnType<T>>
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function useAsync<T extends (...args: unknown[]) => unknown> (fn: T): UseAsync<T> {
|
export default function useAsync<T extends (...args: unknown[]) => unknown>(fn: T): UseAsync<T> {
|
||||||
const active: UseAsync<T>['active'] = ref(false)
|
const active: UseAsync<T>['active'] = ref(false)
|
||||||
|
|
||||||
const run: UseAsync<T>['run'] = async (...args) => {
|
const run: UseAsync<T>['run'] = async (...args) => {
|
||||||
|
|
@ -17,14 +17,16 @@ export default function useAsync<T extends (...args: unknown[]) => unknown> (fn:
|
||||||
try {
|
try {
|
||||||
const result = await fn(...args)
|
const result = await fn(...args)
|
||||||
return result as ReturnType<T>
|
return result as ReturnType<T>
|
||||||
} catch (error) {
|
}
|
||||||
|
catch (error) {
|
||||||
if (isFetchError(error) && error.status === 401) {
|
if (isFetchError(error) && error.status === 401) {
|
||||||
userStorage.remove()
|
userStorage.remove()
|
||||||
await routerPush('login')
|
await routerPush('login')
|
||||||
throw new Error('Unauthorized or token expired')
|
throw new Error('Unauthorized or token expired')
|
||||||
}
|
}
|
||||||
throw error
|
throw error
|
||||||
} finally {
|
}
|
||||||
|
finally {
|
||||||
active.value = false
|
active.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,31 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2020",
|
"target": "ES2020",
|
||||||
"useDefineForClassFields": true,
|
|
||||||
"module": "ESNext",
|
|
||||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"baseUrl": ".",
|
|
||||||
|
|
||||||
/* Bundler mode */
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"allowImportingTsExtensions": true,
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"noEmit": true,
|
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
/* Linting */
|
"useDefineForClassFields": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"noFallthroughCasesInSwitch": true
|
"noEmit": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"skipLibCheck": true
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
|
"references": [
|
||||||
"references": [{ "path": "./tsconfig.node.json" }],
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
],
|
||||||
|
"include": [
|
||||||
|
"src/**/*.ts",
|
||||||
|
"src/**/*.d.ts",
|
||||||
|
"src/**/*.tsx",
|
||||||
|
"src/**/*.vue"
|
||||||
|
],
|
||||||
"ts-node": {
|
"ts-node": {
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"composite": true,
|
"composite": true,
|
||||||
"skipLibCheck": true,
|
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"allowSyntheticDefaultImports": true
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"skipLibCheck": true
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"vite.config.ts",
|
"vite.config.ts",
|
||||||
"cypress.config.ts",
|
"cypress.config.ts",
|
||||||
".eslintrc.js"
|
"eslint.config.js"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
/// <reference types="vitest" />
|
/// <reference types="vitest" />
|
||||||
|
|
||||||
import { fileURLToPath, URL } from 'node:url'
|
import { URL, fileURLToPath } from 'node:url'
|
||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
import { defineConfig } from 'vite'
|
|
||||||
import analyzer from 'rollup-plugin-analyzer'
|
import analyzer from 'rollup-plugin-analyzer'
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue