Merge branch 'develop' into eriese-v3-printing
This commit is contained in:
commit
271686b23b
47 changed files with 1539 additions and 3068 deletions
|
@ -875,7 +875,8 @@
|
||||||
"profile": "https://github.com/BenJamesBen",
|
"profile": "https://github.com/BenJamesBen",
|
||||||
"contributions": [
|
"contributions": [
|
||||||
"code",
|
"code",
|
||||||
"doc"
|
"doc",
|
||||||
|
"bug"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -91,6 +91,7 @@ module.exports = {
|
||||||
],
|
],
|
||||||
env: {
|
env: {
|
||||||
mocha: true,
|
mocha: true,
|
||||||
|
node: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
37
.github/ISSUE_TEMPLATE/01_bug-report.yaml
vendored
37
.github/ISSUE_TEMPLATE/01_bug-report.yaml
vendored
|
@ -1,8 +1,8 @@
|
||||||
---
|
---
|
||||||
name: 🐛 Bug report
|
name: 🐛 Bug report
|
||||||
description: Report a problem, or something that went wrong
|
description: Report a problem, or something that went wrong
|
||||||
title: "[bug]: "
|
title: '[bug]: '
|
||||||
labels: [ "\U0001F41B bug" ]
|
labels: ["\U0001F41B bug"]
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
|
@ -10,7 +10,7 @@ body:
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: desc
|
id: desc
|
||||||
attributes:
|
attributes:
|
||||||
label: "What seems to be the problem? 🤔"
|
label: 'What seems to be the problem? 🤔'
|
||||||
description: Please provide a clear and concise description of the problem you encountered
|
description: Please provide a clear and concise description of the problem you encountered
|
||||||
placeholder: |
|
placeholder: |
|
||||||
When I generate a Simone with my dimensions, the corners of the yoke appear malformed. \
|
When I generate a Simone with my dimensions, the corners of the yoke appear malformed. \
|
||||||
|
@ -18,7 +18,7 @@ body:
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
id: pkg
|
id: pkg
|
||||||
attributes:
|
attributes:
|
||||||
label: "Design / Plugin / Package 🧐"
|
label: 'Design / Plugin / Package 🧐'
|
||||||
description: Do you know what design/plugin/package the bug is in?
|
description: Do you know what design/plugin/package the bug is in?
|
||||||
multiple: true
|
multiple: true
|
||||||
options:
|
options:
|
||||||
|
@ -51,6 +51,7 @@ body:
|
||||||
- designs/lucy
|
- designs/lucy
|
||||||
- designs/lunetius
|
- designs/lunetius
|
||||||
- designs/noble
|
- designs/noble
|
||||||
|
- designs/octoplushy
|
||||||
- designs/paco
|
- designs/paco
|
||||||
- designs/penelope
|
- designs/penelope
|
||||||
- designs/plugintest
|
- designs/plugintest
|
||||||
|
@ -62,7 +63,6 @@ body:
|
||||||
- designs/sven
|
- designs/sven
|
||||||
- designs/tamiko
|
- designs/tamiko
|
||||||
- designs/teagan
|
- designs/teagan
|
||||||
- designs/theo
|
|
||||||
- designs/tiberius
|
- designs/tiberius
|
||||||
- designs/titan
|
- designs/titan
|
||||||
- designs/trayvon
|
- designs/trayvon
|
||||||
|
@ -78,9 +78,9 @@ body:
|
||||||
- plugins/plugin-bundle
|
- plugins/plugin-bundle
|
||||||
- plugins/plugin-bust
|
- plugins/plugin-bust
|
||||||
- plugins/plugin-buttons
|
- plugins/plugin-buttons
|
||||||
|
- plugins/plugin-cutlist
|
||||||
- plugins/plugin-cutonfold
|
- plugins/plugin-cutonfold
|
||||||
- plugins/plugin-dimension
|
- plugins/plugin-dimension
|
||||||
- plugins/plugin-export-dxf
|
|
||||||
- plugins/plugin-flip
|
- plugins/plugin-flip
|
||||||
- plugins/plugin-gore
|
- plugins/plugin-gore
|
||||||
- plugins/plugin-grainline
|
- plugins/plugin-grainline
|
||||||
|
@ -94,33 +94,27 @@ body:
|
||||||
- plugins/plugin-sprinkle
|
- plugins/plugin-sprinkle
|
||||||
- plugins/plugin-svgattr
|
- plugins/plugin-svgattr
|
||||||
- plugins/plugin-theme
|
- plugins/plugin-theme
|
||||||
|
- plugins/plugin-timing
|
||||||
- plugins/plugin-title
|
- plugins/plugin-title
|
||||||
- plugins/plugin-validate
|
|
||||||
- plugins/plugin-versionfree-svg
|
- plugins/plugin-versionfree-svg
|
||||||
- packages/components
|
|
||||||
- packages/config-helpers
|
|
||||||
- packages/core
|
- packages/core
|
||||||
- packages/css-theme
|
|
||||||
- packages/gatsby-remark-jargon
|
|
||||||
- packages/i18n
|
- packages/i18n
|
||||||
- packages/models
|
- packages/models
|
||||||
- packages/mui-theme
|
|
||||||
- packages/new-design
|
- packages/new-design
|
||||||
- packages/pattern-info
|
|
||||||
- packages/prettier-config
|
- packages/prettier-config
|
||||||
|
- packages/rehype-highlight-lines
|
||||||
- packages/rehype-jargon
|
- packages/rehype-jargon
|
||||||
- packages/remark-jargon
|
- packages/snapseries
|
||||||
- packages/utils
|
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
id: patron
|
id: patron
|
||||||
attributes:
|
attributes:
|
||||||
label: Are you a FreeSewing patron? 😃
|
label: Are you a FreeSewing patron? 😃
|
||||||
description: "Patrons support us financially :pray: so they get priority"
|
description: 'Patrons support us financially :pray: so they get priority'
|
||||||
options:
|
options:
|
||||||
- "Yes, I am a tier-2 patron ❤️"
|
- 'Yes, I am a tier-2 patron ❤️'
|
||||||
- "Yes, I am a tier-4 patron ❤️ 💙"
|
- 'Yes, I am a tier-4 patron ❤️ 💙'
|
||||||
- "Yes, I am a tier-8 patron ❤️ 💙 💜"
|
- 'Yes, I am a tier-8 patron ❤️ 💙 💜'
|
||||||
- "No, I am not 😞"
|
- 'No, I am not 😞'
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
|
@ -133,3 +127,4 @@ body:
|
||||||
value: |
|
value: |
|
||||||
Please keep in mind that **FreeSewing is a community project** that depends on **[your support](https://freesewing.org/community/join/)**.
|
Please keep in mind that **FreeSewing is a community project** that depends on **[your support](https://freesewing.org/community/join/)**.
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
20
.github/boring-cyborg.yml
vendored
20
.github/boring-cyborg.yml
vendored
|
@ -1,29 +1,22 @@
|
||||||
##### Labeler #####
|
##### Labeler #####
|
||||||
labelPRBasedOnFilePath:
|
labelPRBasedOnFilePath:
|
||||||
# "label": [ folder or subfolders ]
|
# "label": [ folder or subfolders ]
|
||||||
":package: components": [ packages/components/* ]
|
|
||||||
":package: config-helpers": [ packages/config-helpers/* ]
|
|
||||||
":package: core": [ packages/core/* ]
|
":package: core": [ packages/core/* ]
|
||||||
":package: create-freesewing-pattern": [ packages/create-freesewing-pattern/* ]
|
|
||||||
":package: css-theme": [ packages/css-theme/* ]
|
|
||||||
":package: gatsby-remark-jargon": [ packages/gatsby-remark-jargon/* ]
|
|
||||||
":package: i18n": [ packages/i18n/* ]
|
":package: i18n": [ packages/i18n/* ]
|
||||||
":package: models": [ packages/models/* ]
|
":package: models": [ packages/models/* ]
|
||||||
":package: mui-theme": [ packages/mui-theme/* ]
|
|
||||||
":package: new-design": [ packages/new-design/* ]
|
":package: new-design": [ packages/new-design/* ]
|
||||||
":package: pattern-info": [ packages/pattern-info/* ]
|
|
||||||
":package: prettier-config": [ packages/prettier-config/* ]
|
":package: prettier-config": [ packages/prettier-config/* ]
|
||||||
|
":package: rehype-highlight-lines": [ packages/rehype-highlight-lines/* ]
|
||||||
":package: rehype-jargon": [ packages/rehype-jargon/* ]
|
":package: rehype-jargon": [ packages/rehype-jargon/* ]
|
||||||
":package: remark-jargon": [ packages/remark-jargon/* ]
|
":package: snapseries": [ packages/snapseries/* ]
|
||||||
":package: utils": [ packages/utils/* ]
|
|
||||||
":electric_plug: plugin-banner": [ plugins/plugin-banner/* ]
|
":electric_plug: plugin-banner": [ plugins/plugin-banner/* ]
|
||||||
":electric_plug: plugin-bartack": [ plugins/plugin-bartack/* ]
|
":electric_plug: plugin-bartack": [ plugins/plugin-bartack/* ]
|
||||||
":electric_plug: plugin-bundle": [ plugins/plugin-bundle/* ]
|
":electric_plug: plugin-bundle": [ plugins/plugin-bundle/* ]
|
||||||
":electric_plug: plugin-bust": [ plugins/plugin-bust/* ]
|
":electric_plug: plugin-bust": [ plugins/plugin-bust/* ]
|
||||||
":electric_plug: plugin-buttons": [ plugins/plugin-buttons/* ]
|
":electric_plug: plugin-buttons": [ plugins/plugin-buttons/* ]
|
||||||
|
":electric_plug: plugin-cutlist": [ plugins/plugin-cutlist/* ]
|
||||||
":electric_plug: plugin-cutonfold": [ plugins/plugin-cutonfold/* ]
|
":electric_plug: plugin-cutonfold": [ plugins/plugin-cutonfold/* ]
|
||||||
":electric_plug: plugin-dimension": [ plugins/plugin-dimension/* ]
|
":electric_plug: plugin-dimension": [ plugins/plugin-dimension/* ]
|
||||||
":electric_plug: plugin-export-dxf": [ plugins/plugin-export-dxf/* ]
|
|
||||||
":electric_plug: plugin-flip": [ plugins/plugin-flip/* ]
|
":electric_plug: plugin-flip": [ plugins/plugin-flip/* ]
|
||||||
":electric_plug: plugin-gore": [ plugins/plugin-gore/* ]
|
":electric_plug: plugin-gore": [ plugins/plugin-gore/* ]
|
||||||
":electric_plug: plugin-grainline": [ plugins/plugin-grainline/* ]
|
":electric_plug: plugin-grainline": [ plugins/plugin-grainline/* ]
|
||||||
|
@ -37,18 +30,18 @@ labelPRBasedOnFilePath:
|
||||||
":electric_plug: plugin-sprinkle": [ plugins/plugin-sprinkle/* ]
|
":electric_plug: plugin-sprinkle": [ plugins/plugin-sprinkle/* ]
|
||||||
":electric_plug: plugin-svgattr": [ plugins/plugin-svgattr/* ]
|
":electric_plug: plugin-svgattr": [ plugins/plugin-svgattr/* ]
|
||||||
":electric_plug: plugin-theme": [ plugins/plugin-theme/* ]
|
":electric_plug: plugin-theme": [ plugins/plugin-theme/* ]
|
||||||
|
":electric_plug: plugin-timing": [ plugins/plugin-timing/* ]
|
||||||
":electric_plug: plugin-title": [ plugins/plugin-title/* ]
|
":electric_plug: plugin-title": [ plugins/plugin-title/* ]
|
||||||
":electric_plug: plugin-validate": [ plugins/plugin-validate/* ]
|
|
||||||
":electric_plug: plugin-versionfree-svg": [ plugins/plugin-versionfree-svg/* ]
|
":electric_plug: plugin-versionfree-svg": [ plugins/plugin-versionfree-svg/* ]
|
||||||
":book: documentation": [ markdown/* ]
|
":book: documentation": [ markdown/* ]
|
||||||
":scroll: scripts": [ scripts/* ]
|
":scroll: scripts": [ scripts/* ]
|
||||||
":computer: backend": [ sites/backend/* ]
|
":computer: backend": [ sites/backend/* ]
|
||||||
":computer: dev": [ sites/dev/* ]
|
":computer: dev": [ sites/dev/* ]
|
||||||
|
":computer: email": [ sites/email/* ]
|
||||||
":computer: lab": [ sites/lab/* ]
|
":computer: lab": [ sites/lab/* ]
|
||||||
":computer: org": [ sites/org/* ]
|
":computer: org": [ sites/org/* ]
|
||||||
|
":computer: sanity": [ sites/sanity/* ]
|
||||||
":computer: shared": [ sites/shared/* ]
|
":computer: shared": [ sites/shared/* ]
|
||||||
":computer: strapi": [ sites/strapi/* ]
|
|
||||||
":computer: svgtopdf": [ sites/svgtopdf/* ]
|
|
||||||
":test_tube: tests": [ tests/* ]
|
":test_tube: tests": [ tests/* ]
|
||||||
":gear: configuration": [ "config/*", ".github/*" ]
|
":gear: configuration": [ "config/*", ".github/*" ]
|
||||||
":shirt: aaron": [ designs/aaron/* ]
|
":shirt: aaron": [ designs/aaron/* ]
|
||||||
|
@ -93,7 +86,6 @@ labelPRBasedOnFilePath:
|
||||||
":shirt: sven": [ designs/sven/* ]
|
":shirt: sven": [ designs/sven/* ]
|
||||||
":shirt: tamiko": [ designs/tamiko/* ]
|
":shirt: tamiko": [ designs/tamiko/* ]
|
||||||
":shirt: teagan": [ designs/teagan/* ]
|
":shirt: teagan": [ designs/teagan/* ]
|
||||||
":shirt: theo": [ designs/theo/* ]
|
|
||||||
":shirt: tiberius": [ designs/tiberius/* ]
|
":shirt: tiberius": [ designs/tiberius/* ]
|
||||||
":shirt: titan": [ designs/titan/* ]
|
":shirt: titan": [ designs/titan/* ]
|
||||||
":shirt: trayvon": [ designs/trayvon/* ]
|
":shirt: trayvon": [ designs/trayvon/* ]
|
||||||
|
|
4
.github/workflows/checkdocs.yml
vendored
4
.github/workflows/checkdocs.yml
vendored
|
@ -23,9 +23,9 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
- name: Setup Node.js ${{ matrix.node-version }}
|
- name: Setup Node.js ${{ matrix.node-version }}
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
- name: Install Remark
|
- name: Install Remark
|
||||||
|
|
8
.github/workflows/codeql-analysis.yml
vendored
8
.github/workflows/codeql-analysis.yml
vendored
|
@ -48,11 +48,11 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v1
|
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.
|
||||||
|
@ -63,7 +63,7 @@ jobs:
|
||||||
# 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@v1
|
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
|
||||||
|
@ -77,4 +77,4 @@ jobs:
|
||||||
# make release
|
# make release
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v1
|
uses: github/codeql-action/analyze@v2
|
||||||
|
|
4
.github/workflows/lint.all.yml
vendored
4
.github/workflows/lint.all.yml
vendored
|
@ -19,9 +19,9 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
- name: Setup Node.js ${{ matrix.node-version }}
|
- name: Setup Node.js ${{ matrix.node-version }}
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
|
|
6
.github/workflows/lint.diff.yml
vendored
6
.github/workflows/lint.diff.yml
vendored
|
@ -19,15 +19,15 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Fetch PR base ref
|
- name: Fetch PR base ref
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.base_ref }}
|
ref: ${{ github.base_ref }}
|
||||||
- name: Checkout PR merge ref
|
- name: Checkout PR merge ref
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.ref }}
|
ref: ${{ github.ref }}
|
||||||
- name: Setup Node.js ${{ matrix.node-version }}
|
- name: Setup Node.js ${{ matrix.node-version }}
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
|
|
5
.github/workflows/tests.all.yml
vendored
5
.github/workflows/tests.all.yml
vendored
|
@ -27,11 +27,12 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
- name: Setup Node.js ${{ matrix.node-version }}
|
- name: Setup Node.js ${{ matrix.node-version }}
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
|
cache: 'yarn'
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npx lerna bootstrap
|
run: npx lerna bootstrap
|
||||||
env:
|
env:
|
||||||
|
|
204
CONTRIBUTORS.md
204
CONTRIBUTORS.md
|
@ -9,136 +9,136 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
||||||
<table>
|
<table>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="http://adamrtomkins.github.io/"><img src="https://avatars.githubusercontent.com/u/5709603?v=4?s=100" width="100px;" alt="Adam Tomkins"/><br /><sub><b>Adam Tomkins</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=AdamRTomkins" title="Documentation">📖</a></td>
|
<td align="center"><a href="http://adamrtomkins.github.io/"><img src="https://avatars.githubusercontent.com/u/5709603?v=4?s=100" width="100px;" alt="Adam Tomkins"/><br /><sub><b>Adam Tomkins</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=AdamRTomkins" title="Documentation">📖</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="http://polymerisation-des-concepts.fr/"><img src="https://avatars.githubusercontent.com/u/365999?v=4?s=100" width="100px;" alt="Alexandre Ignjatovic"/><br /><sub><b>Alexandre Ignjatovic</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=bankair" title="Code">💻</a></td>
|
<td align="center"><a href="http://polymerisation-des-concepts.fr/"><img src="https://avatars.githubusercontent.com/u/365999?v=4?s=100" width="100px;" alt="Alexandre Ignjatovic"/><br /><sub><b>Alexandre Ignjatovic</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=bankair" title="Code">💻</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/AlfaLyr"><img src="https://avatars.githubusercontent.com/u/39273729?v=4?s=100" width="100px;" alt="AlfaLyr"/><br /><sub><b>AlfaLyr</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=AlfaLyr" title="Code">💻</a> <a href="#plugin-AlfaLyr" title="Plugin/utility libraries">🔌</a> <a href="#design-AlfaLyr" title="Design">🎨</a></td>
|
<td align="center"><a href="https://github.com/AlfaLyr"><img src="https://avatars.githubusercontent.com/u/39273729?v=4?s=100" width="100px;" alt="AlfaLyr"/><br /><sub><b>AlfaLyr</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=AlfaLyr" title="Code">💻</a> <a href="#plugin-AlfaLyr" title="Plugin/utility libraries">🔌</a> <a href="#design-AlfaLyr" title="Design">🎨</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="http://thelettereph.com"><img src="https://avatars.githubusercontent.com/u/357684?v=4?s=100" width="100px;" alt="Andrew James"/><br /><sub><b>Andrew James</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=ephphatha" title="Documentation">📖</a></td>
|
<td align="center"><a href="http://thelettereph.com"><img src="https://avatars.githubusercontent.com/u/357684?v=4?s=100" width="100px;" alt="Andrew James"/><br /><sub><b>Andrew James</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=ephphatha" title="Documentation">📖</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/annekecaramin"><img src="https://avatars.githubusercontent.com/u/38046191?v=4?s=100" width="100px;" alt="Anneke"/><br /><sub><b>Anneke</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=annekecaramin" title="Documentation">📖</a> <a href="#translation-annekecaramin" title="Translation">🌍</a></td>
|
<td align="center"><a href="https://github.com/annekecaramin"><img src="https://avatars.githubusercontent.com/u/38046191?v=4?s=100" width="100px;" alt="Anneke"/><br /><sub><b>Anneke</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=annekecaramin" title="Documentation">📖</a> <a href="#translation-annekecaramin" title="Translation">🌍</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/anniekao"><img src="https://avatars.githubusercontent.com/u/1550506?v=4?s=100" width="100px;" alt="Annie Kao"/><br /><sub><b>Annie Kao</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=anniekao" title="Documentation">📖</a></td>
|
<td align="center"><a href="https://github.com/anniekao"><img src="https://avatars.githubusercontent.com/u/1550506?v=4?s=100" width="100px;" alt="Annie Kao"/><br /><sub><b>Annie Kao</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=anniekao" title="Documentation">📖</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Anternative"><img src="https://avatars.githubusercontent.com/u/81079850?v=4?s=100" width="100px;" alt="Anternative"/><br /><sub><b>Anternative</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=Anternative" title="Documentation">📖</a></td>
|
<td align="center"><a href="https://github.com/Anternative"><img src="https://avatars.githubusercontent.com/u/81079850?v=4?s=100" width="100px;" alt="Anternative"/><br /><sub><b>Anternative</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=Anternative" title="Documentation">📖</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Quiltmaster"><img src="https://avatars.githubusercontent.com/u/71795777?v=4?s=100" width="100px;" alt="Anthony"/><br /><sub><b>Anthony</b></sub></a><br /><a href="#question-Quiltmaster" title="Answering Questions">💬</a></td>
|
<td align="center"><a href="https://github.com/Quiltmaster"><img src="https://avatars.githubusercontent.com/u/71795777?v=4?s=100" width="100px;" alt="Anthony"/><br /><sub><b>Anthony</b></sub></a><br /><a href="#question-Quiltmaster" title="Answering Questions">💬</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/arigrayzel"><img src="https://avatars.githubusercontent.com/u/33040950?v=4?s=100" width="100px;" alt="Ari Grayzel-student"/><br /><sub><b>Ari Grayzel-student</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=arigrayzel" title="Code">💻</a></td>
|
<td align="center"><a href="https://github.com/arigrayzel"><img src="https://avatars.githubusercontent.com/u/33040950?v=4?s=100" width="100px;" alt="Ari Grayzel-student"/><br /><sub><b>Ari Grayzel-student</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=arigrayzel" title="Code">💻</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Bart-PXL"><img src="https://avatars.githubusercontent.com/u/45118788?v=4?s=100" width="100px;" alt="Bart"/><br /><sub><b>Bart</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=Bart-PXL" title="Documentation">📖</a></td>
|
<td align="center"><a href="https://github.com/Bart-PXL"><img src="https://avatars.githubusercontent.com/u/45118788?v=4?s=100" width="100px;" alt="Bart"/><br /><sub><b>Bart</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=Bart-PXL" title="Documentation">📖</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/BenJamesBen"><img src="https://avatars.githubusercontent.com/u/109869956?v=4?s=100" width="100px;" alt="BenJamesBen"/><br /><sub><b>BenJamesBen</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=BenJamesBen" title="Code">💻</a> <a href="https://github.com/freesewing/freesewing/commits?author=BenJamesBen" title="Documentation">📖</a></td>
|
<td align="center"><a href="https://github.com/BenJamesBen"><img src="https://avatars.githubusercontent.com/u/109869956?v=4?s=100" width="100px;" alt="BenJamesBen"/><br /><sub><b>BenJamesBen</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=BenJamesBen" title="Code">💻</a> <a href="https://github.com/freesewing/freesewing/commits?author=BenJamesBen" title="Documentation">📖</a> <a href="https://github.com/freesewing/freesewing/issues?q=author%3ABenJamesBen" title="Bug reports">🐛</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/camerondubas"><img src="https://avatars.githubusercontent.com/u/6216460?v=4?s=100" width="100px;" alt="Cameron Dubas"/><br /><sub><b>Cameron Dubas</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=camerondubas" title="Documentation">📖</a></td>
|
<td align="center"><a href="https://github.com/camerondubas"><img src="https://avatars.githubusercontent.com/u/6216460?v=4?s=100" width="100px;" alt="Cameron Dubas"/><br /><sub><b>Cameron Dubas</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=camerondubas" title="Documentation">📖</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cabi"><img src="https://avatars.githubusercontent.com/u/2596253?v=4?s=100" width="100px;" alt="Carsten Biebricher"/><br /><sub><b>Carsten Biebricher</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=cabi" title="Documentation">📖</a></td>
|
<td align="center"><a href="https://github.com/cabi"><img src="https://avatars.githubusercontent.com/u/2596253?v=4?s=100" width="100px;" alt="Carsten Biebricher"/><br /><sub><b>Carsten Biebricher</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=cabi" title="Documentation">📖</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cathyzoller"><img src="https://avatars.githubusercontent.com/u/2120275?v=4?s=100" width="100px;" alt="Cathy Zoller"/><br /><sub><b>Cathy Zoller</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=cathyzoller" title="Documentation">📖</a></td>
|
<td align="center"><a href="https://github.com/cathyzoller"><img src="https://avatars.githubusercontent.com/u/2120275?v=4?s=100" width="100px;" alt="Cathy Zoller"/><br /><sub><b>Cathy Zoller</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=cathyzoller" title="Documentation">📖</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Chantalbijoux"><img src="https://avatars.githubusercontent.com/u/39673694?v=4?s=100" width="100px;" alt="Chantal Lapointe"/><br /><sub><b>Chantal Lapointe</b></sub></a><br /><a href="#translation-Chantalbijoux" title="Translation">🌍</a></td>
|
<td align="center"><a href="https://github.com/Chantalbijoux"><img src="https://avatars.githubusercontent.com/u/39673694?v=4?s=100" width="100px;" alt="Chantal Lapointe"/><br /><sub><b>Chantal Lapointe</b></sub></a><br /><a href="#translation-Chantalbijoux" title="Translation">🌍</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dpiquet"><img src="https://avatars.githubusercontent.com/u/4688628?v=4?s=100" width="100px;" alt="Damien PIQUET"/><br /><sub><b>Damien PIQUET</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=dpiquet" title="Code">💻</a></td>
|
<td align="center"><a href="https://github.com/dpiquet"><img src="https://avatars.githubusercontent.com/u/4688628?v=4?s=100" width="100px;" alt="Damien PIQUET"/><br /><sub><b>Damien PIQUET</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=dpiquet" title="Code">💻</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://www.darigovresearch.com/"><img src="https://avatars.githubusercontent.com/u/30328618?v=4?s=100" width="100px;" alt="Darigov Research"/><br /><sub><b>Darigov Research</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=darigovresearch" title="Documentation">📖</a> <a href="#ideas-darigovresearch" title="Ideas, Planning, & Feedback">🤔</a></td>
|
<td align="center"><a href="https://www.darigovresearch.com/"><img src="https://avatars.githubusercontent.com/u/30328618?v=4?s=100" width="100px;" alt="Darigov Research"/><br /><sub><b>Darigov Research</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=darigovresearch" title="Documentation">📖</a> <a href="#ideas-darigovresearch" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ElenaFdR"><img src="https://avatars.githubusercontent.com/u/5113815?v=4?s=100" width="100px;" alt="Elena FdR"/><br /><sub><b>Elena FdR</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=ElenaFdR" title="Documentation">📖</a> <a href="#blog-ElenaFdR" title="Blogposts">📝</a></td>
|
<td align="center"><a href="https://github.com/ElenaFdR"><img src="https://avatars.githubusercontent.com/u/5113815?v=4?s=100" width="100px;" alt="Elena FdR"/><br /><sub><b>Elena FdR</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=ElenaFdR" title="Documentation">📖</a> <a href="#blog-ElenaFdR" title="Blogposts">📝</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://emmanuelnyachoke.com/"><img src="https://avatars.githubusercontent.com/u/1908926?v=4?s=100" width="100px;" alt="Emmanuel Nyachoke"/><br /><sub><b>Emmanuel Nyachoke</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=enyachoke" title="Code">💻</a> <a href="https://github.com/freesewing/freesewing/commits?author=enyachoke" title="Documentation">📖</a></td>
|
<td align="center"><a href="https://emmanuelnyachoke.com/"><img src="https://avatars.githubusercontent.com/u/1908926?v=4?s=100" width="100px;" alt="Emmanuel Nyachoke"/><br /><sub><b>Emmanuel Nyachoke</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=enyachoke" title="Code">💻</a> <a href="https://github.com/freesewing/freesewing/commits?author=enyachoke" title="Documentation">📖</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="http://enochriese.com"><img src="https://avatars.githubusercontent.com/u/5298929?v=4?s=100" width="100px;" alt="Enoch Riese"/><br /><sub><b>Enoch Riese</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=eriese" title="Code">💻</a></td>
|
<td align="center"><a href="http://enochriese.com"><img src="https://avatars.githubusercontent.com/u/5298929?v=4?s=100" width="100px;" alt="Enoch Riese"/><br /><sub><b>Enoch Riese</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=eriese" title="Code">💻</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/EvEkSwed"><img src="https://avatars.githubusercontent.com/u/39723451?v=4?s=100" width="100px;" alt="EvEkSwed"/><br /><sub><b>EvEkSwed</b></sub></a><br /><a href="#translation-EvEkSwed" title="Translation">🌍</a></td>
|
<td align="center"><a href="https://github.com/EvEkSwed"><img src="https://avatars.githubusercontent.com/u/39723451?v=4?s=100" width="100px;" alt="EvEkSwed"/><br /><sub><b>EvEkSwed</b></sub></a><br /><a href="#translation-EvEkSwed" title="Translation">🌍</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Fantastik-Maman"><img src="https://avatars.githubusercontent.com/u/39785382?v=4?s=100" width="100px;" alt="Fantastik-Maman"/><br /><sub><b>Fantastik-Maman</b></sub></a><br /><a href="#translation-Fantastik-Maman" title="Translation">🌍</a></td>
|
<td align="center"><a href="https://github.com/Fantastik-Maman"><img src="https://avatars.githubusercontent.com/u/39785382?v=4?s=100" width="100px;" alt="Fantastik-Maman"/><br /><sub><b>Fantastik-Maman</b></sub></a><br /><a href="#translation-Fantastik-Maman" title="Translation">🌍</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://www.forresto.com/"><img src="https://avatars.githubusercontent.com/u/395307?v=4?s=100" width="100px;" alt="Forrest O."/><br /><sub><b>Forrest O.</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=forresto" title="Documentation">📖</a></td>
|
<td align="center"><a href="https://www.forresto.com/"><img src="https://avatars.githubusercontent.com/u/395307?v=4?s=100" width="100px;" alt="Forrest O."/><br /><sub><b>Forrest O.</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=forresto" title="Documentation">📖</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fmatray"><img src="https://avatars.githubusercontent.com/u/8267716?v=4?s=100" width="100px;" alt="Frédéric"/><br /><sub><b>Frédéric</b></sub></a><br /><a href="#translation-fmatray" title="Translation">🌍</a></td>
|
<td align="center"><a href="https://github.com/fmatray"><img src="https://avatars.githubusercontent.com/u/8267716?v=4?s=100" width="100px;" alt="Frédéric"/><br /><sub><b>Frédéric</b></sub></a><br /><a href="#translation-fmatray" title="Translation">🌍</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://www.linkedin.com/in/glennfmatthews/"><img src="https://avatars.githubusercontent.com/u/5603551?v=4?s=100" width="100px;" alt="Glenn Matthews"/><br /><sub><b>Glenn Matthews</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=glennmatthews" title="Documentation">📖</a></td>
|
<td align="center"><a href="https://www.linkedin.com/in/glennfmatthews/"><img src="https://avatars.githubusercontent.com/u/5603551?v=4?s=100" width="100px;" alt="Glenn Matthews"/><br /><sub><b>Glenn Matthews</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=glennmatthews" title="Documentation">📖</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://greg.technology/"><img src="https://avatars.githubusercontent.com/u/1017304?v=4?s=100" width="100px;" alt="Greg Sadetsky"/><br /><sub><b>Greg Sadetsky</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=gregsadetsky" title="Documentation">📖</a></td>
|
<td align="center"><a href="https://greg.technology/"><img src="https://avatars.githubusercontent.com/u/1017304?v=4?s=100" width="100px;" alt="Greg Sadetsky"/><br /><sub><b>Greg Sadetsky</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=gregsadetsky" title="Documentation">📖</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://kirby.zone"><img src="https://avatars.githubusercontent.com/u/75245963?v=4?s=100" width="100px;" alt="Igor Couto"/><br /><sub><b>Igor Couto</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/issues?q=author%3Aiocouto" title="Bug reports">🐛</a></td>
|
<td align="center"><a href="https://kirby.zone"><img src="https://avatars.githubusercontent.com/u/75245963?v=4?s=100" width="100px;" alt="Igor Couto"/><br /><sub><b>Igor Couto</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/issues?q=author%3Aiocouto" title="Bug reports">🐛</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://bandism.net/"><img src="https://avatars.githubusercontent.com/u/22633385?v=4?s=100" width="100px;" alt="Ikko Ashimine"/><br /><sub><b>Ikko Ashimine</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=eltociear" title="Documentation">📖</a></td>
|
<td align="center"><a href="https://bandism.net/"><img src="https://avatars.githubusercontent.com/u/22633385?v=4?s=100" width="100px;" alt="Ikko Ashimine"/><br /><sub><b>Ikko Ashimine</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=eltociear" title="Documentation">📖</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Irapeke"><img src="https://avatars.githubusercontent.com/u/39604334?v=4?s=100" width="100px;" alt="Irapeke"/><br /><sub><b>Irapeke</b></sub></a><br /><a href="#translation-Irapeke" title="Translation">🌍</a></td>
|
<td align="center"><a href="https://github.com/Irapeke"><img src="https://avatars.githubusercontent.com/u/39604334?v=4?s=100" width="100px;" alt="Irapeke"/><br /><sub><b>Irapeke</b></sub></a><br /><a href="#translation-Irapeke" title="Translation">🌍</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jsawo"><img src="https://avatars.githubusercontent.com/u/1294706?v=4?s=100" width="100px;" alt="Jacek Sawoszczuk"/><br /><sub><b>Jacek Sawoszczuk</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=jsawo" title="Documentation">📖</a></td>
|
<td align="center"><a href="https://github.com/jsawo"><img src="https://avatars.githubusercontent.com/u/1294706?v=4?s=100" width="100px;" alt="Jacek Sawoszczuk"/><br /><sub><b>Jacek Sawoszczuk</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=jsawo" title="Documentation">📖</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jgfichte"><img src="https://avatars.githubusercontent.com/u/1787162?v=4?s=100" width="100px;" alt="Jason Williams"/><br /><sub><b>Jason Williams</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=jgfichte" title="Documentation">📖</a></td>
|
<td align="center"><a href="https://github.com/jgfichte"><img src="https://avatars.githubusercontent.com/u/1787162?v=4?s=100" width="100px;" alt="Jason Williams"/><br /><sub><b>Jason Williams</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=jgfichte" title="Documentation">📖</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jejacks0n"><img src="https://avatars.githubusercontent.com/u/13765?v=4?s=100" width="100px;" alt="Jeremy Jackson"/><br /><sub><b>Jeremy Jackson</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=jejacks0n" title="Code">💻</a></td>
|
<td align="center"><a href="https://github.com/jejacks0n"><img src="https://avatars.githubusercontent.com/u/13765?v=4?s=100" width="100px;" alt="Jeremy Jackson"/><br /><sub><b>Jeremy Jackson</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=jejacks0n" title="Code">💻</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="http://jeroenhoek.nl"><img src="https://avatars.githubusercontent.com/u/683699?v=4?s=100" width="100px;" alt="Jeroen Hoek"/><br /><sub><b>Jeroen Hoek</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=jdhoek" title="Documentation">📖</a></td>
|
<td align="center"><a href="http://jeroenhoek.nl"><img src="https://avatars.githubusercontent.com/u/683699?v=4?s=100" width="100px;" alt="Jeroen Hoek"/><br /><sub><b>Jeroen Hoek</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=jdhoek" title="Documentation">📖</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/joeschofield0"><img src="https://avatars.githubusercontent.com/u/47668691?v=4?s=100" width="100px;" alt="Joe Schofield"/><br /><sub><b>Joe Schofield</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=joeschofield0" title="Documentation">📖</a></td>
|
<td align="center"><a href="https://github.com/joeschofield0"><img src="https://avatars.githubusercontent.com/u/47668691?v=4?s=100" width="100px;" alt="Joe Schofield"/><br /><sub><b>Joe Schofield</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=joeschofield0" title="Documentation">📖</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Joebidido"><img src="https://avatars.githubusercontent.com/u/39796210?v=4?s=100" width="100px;" alt="Joebidido"/><br /><sub><b>Joebidido</b></sub></a><br /><a href="#translation-Joebidido" title="Translation">🌍</a></td>
|
<td align="center"><a href="https://github.com/Joebidido"><img src="https://avatars.githubusercontent.com/u/39796210?v=4?s=100" width="100px;" alt="Joebidido"/><br /><sub><b>Joebidido</b></sub></a><br /><a href="#translation-Joebidido" title="Translation">🌍</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://joost.at/"><img src="https://avatars.githubusercontent.com/u/1708494?v=4?s=100" width="100px;" alt="Joost De Cock"/><br /><sub><b>Joost De Cock</b></sub></a><br /><a href="#maintenance-joostdecock" title="Maintenance">🚧</a></td>
|
<td align="center"><a href="https://joost.at/"><img src="https://avatars.githubusercontent.com/u/1708494?v=4?s=100" width="100px;" alt="Joost De Cock"/><br /><sub><b>Joost De Cock</b></sub></a><br /><a href="#maintenance-joostdecock" title="Maintenance">🚧</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/joshessman"><img src="https://avatars.githubusercontent.com/u/9941074?v=4?s=100" width="100px;" alt="Josh Essman"/><br /><sub><b>Josh Essman</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=joshessman" title="Documentation">📖</a></td>
|
<td align="center"><a href="https://github.com/joshessman"><img src="https://avatars.githubusercontent.com/u/9941074?v=4?s=100" width="100px;" alt="Josh Essman"/><br /><sub><b>Josh Essman</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=joshessman" title="Documentation">📖</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="http://www.earth.li/~kake/"><img src="https://avatars.githubusercontent.com/u/1956810?v=4?s=100" width="100px;" alt="Kake"/><br /><sub><b>Kake</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=KakeLP" title="Documentation">📖</a></td>
|
<td align="center"><a href="http://www.earth.li/~kake/"><img src="https://avatars.githubusercontent.com/u/1956810?v=4?s=100" width="100px;" alt="Kake"/><br /><sub><b>Kake</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=KakeLP" title="Documentation">📖</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://twitter.com/kapunahele"><img src="https://avatars.githubusercontent.com/u/4116963?v=4?s=100" width="100px;" alt="Kapunahele Wong"/><br /><sub><b>Kapunahele Wong</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=kapunahelewong" title="Documentation">📖</a></td>
|
<td align="center"><a href="https://twitter.com/kapunahele"><img src="https://avatars.githubusercontent.com/u/4116963?v=4?s=100" width="100px;" alt="Kapunahele Wong"/><br /><sub><b>Kapunahele Wong</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=kapunahelewong" title="Documentation">📖</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tangerineshark"><img src="https://avatars.githubusercontent.com/u/70777269?v=4?s=100" width="100px;" alt="Karen"/><br /><sub><b>Karen</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=tangerineshark" title="Documentation">📖</a> <a href="#eventOrganizing-tangerineshark" title="Event Organizing">📋</a></td>
|
<td align="center"><a href="https://github.com/tangerineshark"><img src="https://avatars.githubusercontent.com/u/70777269?v=4?s=100" width="100px;" alt="Karen"/><br /><sub><b>Karen</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=tangerineshark" title="Documentation">📖</a> <a href="#eventOrganizing-tangerineshark" title="Event Organizing">📋</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mcgnly"><img src="https://avatars.githubusercontent.com/u/5653631?v=4?s=100" width="100px;" alt="Katie McGinley"/><br /><sub><b>Katie McGinley</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=mcgnly" title="Documentation">📖</a></td>
|
<td align="center"><a href="https://github.com/mcgnly"><img src="https://avatars.githubusercontent.com/u/5653631?v=4?s=100" width="100px;" alt="Katie McGinley"/><br /><sub><b>Katie McGinley</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=mcgnly" title="Documentation">📖</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="http://www.kieranklaassen.com/"><img src="https://avatars.githubusercontent.com/u/209089?v=4?s=100" width="100px;" alt="Kieran Klaassen"/><br /><sub><b>Kieran Klaassen</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=kieranklaassen" title="Code">💻</a></td>
|
<td align="center"><a href="http://www.kieranklaassen.com/"><img src="https://avatars.githubusercontent.com/u/209089?v=4?s=100" width="100px;" alt="Kieran Klaassen"/><br /><sub><b>Kieran Klaassen</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=kieranklaassen" title="Code">💻</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Kittycatou"><img src="https://avatars.githubusercontent.com/u/48165583?v=4?s=100" width="100px;" alt="Kittycatou"/><br /><sub><b>Kittycatou</b></sub></a><br /><a href="#translation-Kittycatou" title="Translation">🌍</a></td>
|
<td align="center"><a href="https://github.com/Kittycatou"><img src="https://avatars.githubusercontent.com/u/48165583?v=4?s=100" width="100px;" alt="Kittycatou"/><br /><sub><b>Kittycatou</b></sub></a><br /><a href="#translation-Kittycatou" title="Translation">🌍</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://www.krishoward.org/"><img src="https://avatars.githubusercontent.com/u/5946286?v=4?s=100" width="100px;" alt="Kris"/><br /><sub><b>Kris</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=web-goddess" title="Documentation">📖</a></td>
|
<td align="center"><a href="https://www.krishoward.org/"><img src="https://avatars.githubusercontent.com/u/5946286?v=4?s=100" width="100px;" alt="Kris"/><br /><sub><b>Kris</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=web-goddess" title="Documentation">📖</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/kristinruben"><img src="https://avatars.githubusercontent.com/u/17237479?v=4?s=100" width="100px;" alt="Kristin Ruben"/><br /><sub><b>Kristin Ruben</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=kristinruben" title="Code">💻</a></td>
|
<td align="center"><a href="https://github.com/kristinruben"><img src="https://avatars.githubusercontent.com/u/17237479?v=4?s=100" width="100px;" alt="Kristin Ruben"/><br /><sub><b>Kristin Ruben</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=kristinruben" title="Code">💻</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Loudepeuter"><img src="https://avatars.githubusercontent.com/u/38081954?v=4?s=100" width="100px;" alt="Loudepeuter"/><br /><sub><b>Loudepeuter</b></sub></a><br /><a href="#translation-Loudepeuter" title="Translation">🌍</a></td>
|
<td align="center"><a href="https://github.com/Loudepeuter"><img src="https://avatars.githubusercontent.com/u/38081954?v=4?s=100" width="100px;" alt="Loudepeuter"/><br /><sub><b>Loudepeuter</b></sub></a><br /><a href="#translation-Loudepeuter" title="Translation">🌍</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lucibytes"><img src="https://avatars.githubusercontent.com/u/77203781?v=4?s=100" width="100px;" alt="Lucian"/><br /><sub><b>Lucian</b></sub></a><br /><a href="#eventOrganizing-lucibytes" title="Event Organizing">📋</a></td>
|
<td align="center"><a href="https://github.com/lucibytes"><img src="https://avatars.githubusercontent.com/u/77203781?v=4?s=100" width="100px;" alt="Lucian"/><br /><sub><b>Lucian</b></sub></a><br /><a href="#eventOrganizing-lucibytes" title="Event Organizing">📋</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/manufakturedelweiss"><img src="https://avatars.githubusercontent.com/u/38063391?v=4?s=100" width="100px;" alt="Marcus"/><br /><sub><b>Marcus</b></sub></a><br /><a href="#translation-manufakturedelweiss" title="Translation">🌍</a></td>
|
<td align="center"><a href="https://github.com/manufakturedelweiss"><img src="https://avatars.githubusercontent.com/u/38063391?v=4?s=100" width="100px;" alt="Marcus"/><br /><sub><b>Marcus</b></sub></a><br /><a href="#translation-manufakturedelweiss" title="Translation">🌍</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/martintribo"><img src="https://avatars.githubusercontent.com/u/1613442?v=4?s=100" width="100px;" alt="Martin Tribo"/><br /><sub><b>Martin Tribo</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=martintribo" title="Documentation">📖</a></td>
|
<td align="center"><a href="https://github.com/martintribo"><img src="https://avatars.githubusercontent.com/u/1613442?v=4?s=100" width="100px;" alt="Martin Tribo"/><br /><sub><b>Martin Tribo</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=martintribo" title="Documentation">📖</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/nadege"><img src="https://avatars.githubusercontent.com/u/3792171?v=4?s=100" width="100px;" alt="Nadege Michel"/><br /><sub><b>Nadege Michel</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=nadege" title="Tests">⚠️</a> <a href="https://github.com/freesewing/freesewing/commits?author=nadege" title="Documentation">📖</a></td>
|
<td align="center"><a href="https://github.com/nadege"><img src="https://avatars.githubusercontent.com/u/3792171?v=4?s=100" width="100px;" alt="Nadege Michel"/><br /><sub><b>Nadege Michel</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=nadege" title="Tests">⚠️</a> <a href="https://github.com/freesewing/freesewing/commits?author=nadege" title="Documentation">📖</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/nataliasayang"><img src="https://avatars.githubusercontent.com/u/48160791?v=4?s=100" width="100px;" alt="Natalia"/><br /><sub><b>Natalia</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=nataliasayang" title="Code">💻</a> <a href="#design-nataliasayang" title="Design">🎨</a> <a href="#blog-nataliasayang" title="Blogposts">📝</a></td>
|
<td align="center"><a href="https://github.com/nataliasayang"><img src="https://avatars.githubusercontent.com/u/48160791?v=4?s=100" width="100px;" alt="Natalia"/><br /><sub><b>Natalia</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=nataliasayang" title="Code">💻</a> <a href="#design-nataliasayang" title="Design">🎨</a> <a href="#blog-nataliasayang" title="Blogposts">📝</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="http://yergler.net/"><img src="https://avatars.githubusercontent.com/u/510875?v=4?s=100" width="100px;" alt="Nathan Yergler"/><br /><sub><b>Nathan Yergler</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=nyergler" title="Documentation">📖</a></td>
|
<td align="center"><a href="http://yergler.net/"><img src="https://avatars.githubusercontent.com/u/510875?v=4?s=100" width="100px;" alt="Nathan Yergler"/><br /><sub><b>Nathan Yergler</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=nyergler" title="Documentation">📖</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/nicholasdower"><img src="https://avatars.githubusercontent.com/u/9117775?v=4?s=100" width="100px;" alt="Nick Dower"/><br /><sub><b>Nick Dower</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=nicholasdower" title="Documentation">📖</a> <a href="https://github.com/freesewing/freesewing/commits?author=nicholasdower" title="Code">💻</a> <a href="https://github.com/freesewing/freesewing/issues?q=author%3Anicholasdower" title="Bug reports">🐛</a></td>
|
<td align="center"><a href="https://github.com/nicholasdower"><img src="https://avatars.githubusercontent.com/u/9117775?v=4?s=100" width="100px;" alt="Nick Dower"/><br /><sub><b>Nick Dower</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=nicholasdower" title="Documentation">📖</a> <a href="https://github.com/freesewing/freesewing/commits?author=nicholasdower" title="Code">💻</a> <a href="https://github.com/freesewing/freesewing/issues?q=author%3Anicholasdower" title="Bug reports">🐛</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/nchilada"><img src="https://avatars.githubusercontent.com/u/692925?v=4?s=100" width="100px;" alt="Nikhil Chelliah"/><br /><sub><b>Nikhil Chelliah</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=nchilada" title="Documentation">📖</a></td>
|
<td align="center"><a href="https://github.com/nchilada"><img src="https://avatars.githubusercontent.com/u/692925?v=4?s=100" width="100px;" alt="Nikhil Chelliah"/><br /><sub><b>Nikhil Chelliah</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=nchilada" title="Documentation">📖</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/OysteinHoiby"><img src="https://avatars.githubusercontent.com/u/49735055?v=4?s=100" width="100px;" alt="OysteinHoiby"/><br /><sub><b>OysteinHoiby</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=OysteinHoiby" title="Code">💻</a></td>
|
<td align="center"><a href="https://github.com/OysteinHoiby"><img src="https://avatars.githubusercontent.com/u/49735055?v=4?s=100" width="100px;" alt="OysteinHoiby"/><br /><sub><b>OysteinHoiby</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=OysteinHoiby" title="Code">💻</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://pat.forringer.com/"><img src="https://avatars.githubusercontent.com/u/136456?v=4?s=100" width="100px;" alt="Patrick Forringer"/><br /><sub><b>Patrick Forringer</b></sub></a><br /><a href="#plugin-destos" title="Plugin/utility libraries">🔌</a></td>
|
<td align="center"><a href="https://pat.forringer.com/"><img src="https://avatars.githubusercontent.com/u/136456?v=4?s=100" width="100px;" alt="Patrick Forringer"/><br /><sub><b>Patrick Forringer</b></sub></a><br /><a href="#plugin-destos" title="Plugin/utility libraries">🔌</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="http://pd75.github.io/"><img src="https://avatars.githubusercontent.com/u/10294795?v=4?s=100" width="100px;" alt="Paul"/><br /><sub><b>Paul</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=PD75" title="Documentation">📖</a> <a href="#blog-PD75" title="Blogposts">📝</a> <a href="#translation-PD75" title="Translation">🌍</a></td>
|
<td align="center"><a href="http://pd75.github.io/"><img src="https://avatars.githubusercontent.com/u/10294795?v=4?s=100" width="100px;" alt="Paul"/><br /><sub><b>Paul</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=PD75" title="Documentation">📖</a> <a href="#blog-PD75" title="Blogposts">📝</a> <a href="#translation-PD75" title="Translation">🌍</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/phillipthelen"><img src="https://avatars.githubusercontent.com/u/298062?v=4?s=100" width="100px;" alt="Phillip Thelen"/><br /><sub><b>Phillip Thelen</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=phillipthelen" title="Code">💻</a></td>
|
<td align="center"><a href="https://github.com/phillipthelen"><img src="https://avatars.githubusercontent.com/u/298062?v=4?s=100" width="100px;" alt="Phillip Thelen"/><br /><sub><b>Phillip Thelen</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=phillipthelen" title="Code">💻</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Pixieish"><img src="https://avatars.githubusercontent.com/u/32991415?v=4?s=100" width="100px;" alt="Pixieish"/><br /><sub><b>Pixieish</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=Pixieish" title="Documentation">📖</a></td>
|
<td align="center"><a href="https://github.com/Pixieish"><img src="https://avatars.githubusercontent.com/u/32991415?v=4?s=100" width="100px;" alt="Pixieish"/><br /><sub><b>Pixieish</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=Pixieish" title="Documentation">📖</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://www.uza.be/persoon/prof-dr-sorcha-ni-dhubhghaill"><img src="https://avatars.githubusercontent.com/u/30624634?v=4?s=100" width="100px;" alt="Prof. dr. Sorcha Ní Dhubhghaill"/><br /><sub><b>Prof. dr. Sorcha Ní Dhubhghaill</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=sorchanidhubhghaill" title="Documentation">📖</a></td>
|
<td align="center"><a href="https://www.uza.be/persoon/prof-dr-sorcha-ni-dhubhghaill"><img src="https://avatars.githubusercontent.com/u/30624634?v=4?s=100" width="100px;" alt="Prof. dr. Sorcha Ní Dhubhghaill"/><br /><sub><b>Prof. dr. Sorcha Ní Dhubhghaill</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=sorchanidhubhghaill" title="Documentation">📖</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/QuentinFelix"><img src="https://avatars.githubusercontent.com/u/5288091?v=4?s=100" width="100px;" alt="Quentin FELIX"/><br /><sub><b>Quentin FELIX</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=QuentinFelix" title="Code">💻</a> <a href="#design-QuentinFelix" title="Design">🎨</a></td>
|
<td align="center"><a href="https://github.com/QuentinFelix"><img src="https://avatars.githubusercontent.com/u/5288091?v=4?s=100" width="100px;" alt="Quentin FELIX"/><br /><sub><b>Quentin FELIX</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=QuentinFelix" title="Code">💻</a> <a href="#design-QuentinFelix" title="Design">🎨</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/RikHekker"><img src="https://avatars.githubusercontent.com/u/31843274?v=4?s=100" width="100px;" alt="Rik Hekker"/><br /><sub><b>Rik Hekker</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/issues?q=author%3ARikHekker" title="Bug reports">🐛</a></td>
|
<td align="center"><a href="https://github.com/RikHekker"><img src="https://avatars.githubusercontent.com/u/31843274?v=4?s=100" width="100px;" alt="Rik Hekker"/><br /><sub><b>Rik Hekker</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/issues?q=author%3ARikHekker" title="Bug reports">🐛</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="http://resume.livingston-gray.com/faq.html"><img src="https://avatars.githubusercontent.com/u/6462?v=4?s=100" width="100px;" alt="Sam Livingston-Gray"/><br /><sub><b>Sam Livingston-Gray</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=geeksam" title="Documentation">📖</a></td>
|
<td align="center"><a href="http://resume.livingston-gray.com/faq.html"><img src="https://avatars.githubusercontent.com/u/6462?v=4?s=100" width="100px;" alt="Sam Livingston-Gray"/><br /><sub><b>Sam Livingston-Gray</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=geeksam" title="Documentation">📖</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sannek"><img src="https://avatars.githubusercontent.com/u/17491062?v=4?s=100" width="100px;" alt="Sanne"/><br /><sub><b>Sanne</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=sannek" title="Code">💻</a> <a href="https://github.com/freesewing/freesewing/commits?author=sannek" title="Documentation">📖</a></td>
|
<td align="center"><a href="https://github.com/sannek"><img src="https://avatars.githubusercontent.com/u/17491062?v=4?s=100" width="100px;" alt="Sanne"/><br /><sub><b>Sanne</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=sannek" title="Code">💻</a> <a href="https://github.com/freesewing/freesewing/commits?author=sannek" title="Documentation">📖</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Tyrannogina"><img src="https://avatars.githubusercontent.com/u/19556565?v=4?s=100" width="100px;" alt="Sara Latorre"/><br /><sub><b>Sara Latorre</b></sub></a><br /><a href="#translation-Tyrannogina" title="Translation">🌍</a></td>
|
<td align="center"><a href="https://github.com/Tyrannogina"><img src="https://avatars.githubusercontent.com/u/19556565?v=4?s=100" width="100px;" alt="Sara Latorre"/><br /><sub><b>Sara Latorre</b></sub></a><br /><a href="#translation-Tyrannogina" title="Translation">🌍</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/SeaZeeZee"><img src="https://avatars.githubusercontent.com/u/86711383?v=4?s=100" width="100px;" alt="SeaZeeZee"/><br /><sub><b>SeaZeeZee</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=SeaZeeZee" title="Documentation">📖</a> <a href="https://github.com/freesewing/freesewing/commits?author=SeaZeeZee" title="Code">💻</a></td>
|
<td align="center"><a href="https://github.com/SeaZeeZee"><img src="https://avatars.githubusercontent.com/u/86711383?v=4?s=100" width="100px;" alt="SeaZeeZee"/><br /><sub><b>SeaZeeZee</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=SeaZeeZee" title="Documentation">📖</a> <a href="https://github.com/freesewing/freesewing/commits?author=SeaZeeZee" title="Code">💻</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/SimonbJohnson"><img src="https://avatars.githubusercontent.com/u/2110742?v=4?s=100" width="100px;" alt="SimonbJohnson"/><br /><sub><b>SimonbJohnson</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/issues?q=author%3ASimonbJohnson" title="Bug reports">🐛</a></td>
|
<td align="center"><a href="https://github.com/SimonbJohnson"><img src="https://avatars.githubusercontent.com/u/2110742?v=4?s=100" width="100px;" alt="SimonbJohnson"/><br /><sub><b>SimonbJohnson</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/issues?q=author%3ASimonbJohnson" title="Bug reports">🐛</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/SirCharlotte"><img src="https://avatars.githubusercontent.com/u/63847870?v=4?s=100" width="100px;" alt="SirCharlotte"/><br /><sub><b>SirCharlotte</b></sub></a><br /><a href="#translation-SirCharlotte" title="Translation">🌍</a></td>
|
<td align="center"><a href="https://github.com/SirCharlotte"><img src="https://avatars.githubusercontent.com/u/63847870?v=4?s=100" width="100px;" alt="SirCharlotte"/><br /><sub><b>SirCharlotte</b></sub></a><br /><a href="#translation-SirCharlotte" title="Translation">🌍</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://www.instagram.com/celine_mge/"><img src="https://avatars.githubusercontent.com/u/57619777?v=4?s=100" width="100px;" alt="Slylele"/><br /><sub><b>Slylele</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=Slylele" title="Documentation">📖</a> <a href="#translation-Slylele" title="Translation">🌍</a></td>
|
<td align="center"><a href="https://www.instagram.com/celine_mge/"><img src="https://avatars.githubusercontent.com/u/57619777?v=4?s=100" width="100px;" alt="Slylele"/><br /><sub><b>Slylele</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=Slylele" title="Documentation">📖</a> <a href="#translation-Slylele" title="Translation">🌍</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Soazillon"><img src="https://avatars.githubusercontent.com/u/40845940?v=4?s=100" width="100px;" alt="Soazillon"/><br /><sub><b>Soazillon</b></sub></a><br /><a href="#translation-Soazillon" title="Translation">🌍</a></td>
|
<td align="center"><a href="https://github.com/Soazillon"><img src="https://avatars.githubusercontent.com/u/40845940?v=4?s=100" width="100px;" alt="Soazillon"/><br /><sub><b>Soazillon</b></sub></a><br /><a href="#translation-Soazillon" title="Translation">🌍</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/SoneaTheBest"><img src="https://avatars.githubusercontent.com/u/64635425?v=4?s=100" width="100px;" alt="SoneaTheBest"/><br /><sub><b>SoneaTheBest</b></sub></a><br /><a href="#translation-SoneaTheBest" title="Translation">🌍</a></td>
|
<td align="center"><a href="https://github.com/SoneaTheBest"><img src="https://avatars.githubusercontent.com/u/64635425?v=4?s=100" width="100px;" alt="SoneaTheBest"/><br /><sub><b>SoneaTheBest</b></sub></a><br /><a href="#translation-SoneaTheBest" title="Translation">🌍</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="http://metafly.info/"><img src="https://avatars.githubusercontent.com/u/961256?v=4?s=100" width="100px;" alt="Stefan Sydow"/><br /><sub><b>Stefan Sydow</b></sub></a><br /><a href="#translation-stsydow" title="Translation">🌍</a> <a href="https://github.com/freesewing/freesewing/commits?author=stsydow" title="Documentation">📖</a> <a href="https://github.com/freesewing/freesewing/commits?author=stsydow" title="Code">💻</a></td>
|
<td align="center"><a href="http://metafly.info/"><img src="https://avatars.githubusercontent.com/u/961256?v=4?s=100" width="100px;" alt="Stefan Sydow"/><br /><sub><b>Stefan Sydow</b></sub></a><br /><a href="#translation-stsydow" title="Translation">🌍</a> <a href="https://github.com/freesewing/freesewing/commits?author=stsydow" title="Documentation">📖</a> <a href="https://github.com/freesewing/freesewing/commits?author=stsydow" title="Code">💻</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/TriploidTree"><img src="https://avatars.githubusercontent.com/u/4170521?v=4?s=100" width="100px;" alt="Tríona"/><br /><sub><b>Tríona</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=TriploidTree" title="Documentation">📖</a></td>
|
<td align="center"><a href="https://github.com/TriploidTree"><img src="https://avatars.githubusercontent.com/u/4170521?v=4?s=100" width="100px;" alt="Tríona"/><br /><sub><b>Tríona</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=TriploidTree" title="Documentation">📖</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/theUnmutual"><img src="https://avatars.githubusercontent.com/u/22374635?v=4?s=100" width="100px;" alt="Unmutual"/><br /><sub><b>Unmutual</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=theUnmutual" title="Documentation">📖</a></td>
|
<td align="center"><a href="https://github.com/theUnmutual"><img src="https://avatars.githubusercontent.com/u/22374635?v=4?s=100" width="100px;" alt="Unmutual"/><br /><sub><b>Unmutual</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=theUnmutual" title="Documentation">📖</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/woutervdub"><img src="https://avatars.githubusercontent.com/u/24414629?v=4?s=100" width="100px;" alt="Wouter van Wageningen"/><br /><sub><b>Wouter van Wageningen</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=woutervdub" title="Code">💻</a> <a href="#design-woutervdub" title="Design">🎨</a> <a href="#tool-woutervdub" title="Tools">🔧</a></td>
|
<td align="center"><a href="https://github.com/woutervdub"><img src="https://avatars.githubusercontent.com/u/24414629?v=4?s=100" width="100px;" alt="Wouter van Wageningen"/><br /><sub><b>Wouter van Wageningen</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=woutervdub" title="Code">💻</a> <a href="#design-woutervdub" title="Design">🎨</a> <a href="#tool-woutervdub" title="Tools">🔧</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/amysews"><img src="https://avatars.githubusercontent.com/u/25280778?v=4?s=100" width="100px;" alt="amysews"/><br /><sub><b>amysews</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=amysews" title="Documentation">📖</a></td>
|
<td align="center"><a href="https://github.com/amysews"><img src="https://avatars.githubusercontent.com/u/25280778?v=4?s=100" width="100px;" alt="amysews"/><br /><sub><b>amysews</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=amysews" title="Documentation">📖</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/anna-puk"><img src="https://avatars.githubusercontent.com/u/100537439?v=4?s=100" width="100px;" alt="anna-puk"/><br /><sub><b>anna-puk</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=anna-puk" title="Code">💻</a></td>
|
<td align="center"><a href="https://github.com/anna-puk"><img src="https://avatars.githubusercontent.com/u/100537439?v=4?s=100" width="100px;" alt="anna-puk"/><br /><sub><b>anna-puk</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=anna-puk" title="Code">💻</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/beautifulsummermoon"><img src="https://avatars.githubusercontent.com/u/40396388?v=4?s=100" width="100px;" alt="beautifulsummermoon"/><br /><sub><b>beautifulsummermoon</b></sub></a><br /><a href="#translation-beautifulsummermoon" title="Translation">🌍</a></td>
|
<td align="center"><a href="https://github.com/beautifulsummermoon"><img src="https://avatars.githubusercontent.com/u/40396388?v=4?s=100" width="100px;" alt="beautifulsummermoon"/><br /><sub><b>beautifulsummermoon</b></sub></a><br /><a href="#translation-beautifulsummermoon" title="Translation">🌍</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/berce"><img src="https://avatars.githubusercontent.com/u/10439709?v=4?s=100" width="100px;" alt="berce"/><br /><sub><b>berce</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=berce" title="Documentation">📖</a></td>
|
<td align="center"><a href="https://github.com/berce"><img src="https://avatars.githubusercontent.com/u/10439709?v=4?s=100" width="100px;" alt="berce"/><br /><sub><b>berce</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=berce" title="Documentation">📖</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/biou"><img src="https://avatars.githubusercontent.com/u/1340376?v=4?s=100" width="100px;" alt="biou"/><br /><sub><b>biou</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=biou" title="Code">💻</a></td>
|
<td align="center"><a href="https://github.com/biou"><img src="https://avatars.githubusercontent.com/u/1340376?v=4?s=100" width="100px;" alt="biou"/><br /><sub><b>biou</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=biou" title="Code">💻</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/bobgeorgethe3rd"><img src="https://avatars.githubusercontent.com/u/16866285?v=4?s=100" width="100px;" alt="bobgeorgethe3rd"/><br /><sub><b>bobgeorgethe3rd</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=bobgeorgethe3rd" title="Code">💻</a> <a href="https://github.com/freesewing/freesewing/commits?author=bobgeorgethe3rd" title="Documentation">📖</a> <a href="#design-bobgeorgethe3rd" title="Design">🎨</a></td>
|
<td align="center"><a href="https://github.com/bobgeorgethe3rd"><img src="https://avatars.githubusercontent.com/u/16866285?v=4?s=100" width="100px;" alt="bobgeorgethe3rd"/><br /><sub><b>bobgeorgethe3rd</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=bobgeorgethe3rd" title="Code">💻</a> <a href="https://github.com/freesewing/freesewing/commits?author=bobgeorgethe3rd" title="Documentation">📖</a> <a href="#design-bobgeorgethe3rd" title="Design">🎨</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/brmlyklr"><img src="https://avatars.githubusercontent.com/u/22308713?v=4?s=100" width="100px;" alt="brmlyklr"/><br /><sub><b>brmlyklr</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=brmlyklr" title="Documentation">📖</a></td>
|
<td align="center"><a href="https://github.com/brmlyklr"><img src="https://avatars.githubusercontent.com/u/22308713?v=4?s=100" width="100px;" alt="brmlyklr"/><br /><sub><b>brmlyklr</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=brmlyklr" title="Documentation">📖</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="http://www.chrisbarrett.fr"><img src="https://avatars.githubusercontent.com/u/2373249?v=4?s=100" width="100px;" alt="chri5b"/><br /><sub><b>chri5b</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=chri5b" title="Code">💻</a> <a href="https://github.com/freesewing/freesewing/commits?author=chri5b" title="Tests">⚠️</a></td>
|
<td align="center"><a href="http://www.chrisbarrett.fr"><img src="https://avatars.githubusercontent.com/u/2373249?v=4?s=100" width="100px;" alt="chri5b"/><br /><sub><b>chri5b</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=chri5b" title="Code">💻</a> <a href="https://github.com/freesewing/freesewing/commits?author=chri5b" title="Tests">⚠️</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dingcycle"><img src="https://avatars.githubusercontent.com/u/1681985?v=4?s=100" width="100px;" alt="dingcycle"/><br /><sub><b>dingcycle</b></sub></a><br /><a href="#translation-dingcycle" title="Translation">🌍</a></td>
|
<td align="center"><a href="https://github.com/dingcycle"><img src="https://avatars.githubusercontent.com/u/1681985?v=4?s=100" width="100px;" alt="dingcycle"/><br /><sub><b>dingcycle</b></sub></a><br /><a href="#translation-dingcycle" title="Translation">🌍</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/drowned-in-books"><img src="https://avatars.githubusercontent.com/u/100040772?v=4?s=100" width="100px;" alt="drowned-in-books"/><br /><sub><b>drowned-in-books</b></sub></a><br /><a href="#question-drowned-in-books" title="Answering Questions">💬</a></td>
|
<td align="center"><a href="https://github.com/drowned-in-books"><img src="https://avatars.githubusercontent.com/u/100040772?v=4?s=100" width="100px;" alt="drowned-in-books"/><br /><sub><b>drowned-in-books</b></sub></a><br /><a href="#question-drowned-in-books" title="Answering Questions">💬</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/econo202"><img src="https://avatars.githubusercontent.com/u/34138153?v=4?s=100" width="100px;" alt="econo202"/><br /><sub><b>econo202</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=econo202" title="Documentation">📖</a></td>
|
<td align="center"><a href="https://github.com/econo202"><img src="https://avatars.githubusercontent.com/u/34138153?v=4?s=100" width="100px;" alt="econo202"/><br /><sub><b>econo202</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=econo202" title="Documentation">📖</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ericamattos"><img src="https://avatars.githubusercontent.com/u/4341417?v=4?s=100" width="100px;" alt="ericamattos"/><br /><sub><b>ericamattos</b></sub></a><br /><a href="#translation-ericamattos" title="Translation">🌍</a></td>
|
<td align="center"><a href="https://github.com/ericamattos"><img src="https://avatars.githubusercontent.com/u/4341417?v=4?s=100" width="100px;" alt="ericamattos"/><br /><sub><b>ericamattos</b></sub></a><br /><a href="#translation-ericamattos" title="Translation">🌍</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fightingrabbit"><img src="https://avatars.githubusercontent.com/u/25751445?v=4?s=100" width="100px;" alt="fightingrabbit"/><br /><sub><b>fightingrabbit</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=fightingrabbit" title="Code">💻</a></td>
|
<td align="center"><a href="https://github.com/fightingrabbit"><img src="https://avatars.githubusercontent.com/u/25751445?v=4?s=100" width="100px;" alt="fightingrabbit"/><br /><sub><b>fightingrabbit</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=fightingrabbit" title="Code">💻</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/DocSpencer77"><img src="https://avatars.githubusercontent.com/u/43393580?v=4?s=100" width="100px;" alt="gaylyndie"/><br /><sub><b>gaylyndie</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=DocSpencer77" title="Documentation">📖</a></td>
|
<td align="center"><a href="https://github.com/DocSpencer77"><img src="https://avatars.githubusercontent.com/u/43393580?v=4?s=100" width="100px;" alt="gaylyndie"/><br /><sub><b>gaylyndie</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=DocSpencer77" title="Documentation">📖</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/grimlokason"><img src="https://avatars.githubusercontent.com/u/5112238?v=4?s=100" width="100px;" alt="grimlokason"/><br /><sub><b>grimlokason</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=grimlokason" title="Code">💻</a></td>
|
<td align="center"><a href="https://github.com/grimlokason"><img src="https://avatars.githubusercontent.com/u/5112238?v=4?s=100" width="100px;" alt="grimlokason"/><br /><sub><b>grimlokason</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=grimlokason" title="Code">💻</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://weblog.redisdead.net"><img src="https://avatars.githubusercontent.com/u/6494414?v=4?s=100" width="100px;" alt="hellgy"/><br /><sub><b>hellgy</b></sub></a><br /><a href="#design-hellgy" title="Design">🎨</a></td>
|
<td align="center"><a href="https://weblog.redisdead.net"><img src="https://avatars.githubusercontent.com/u/6494414?v=4?s=100" width="100px;" alt="hellgy"/><br /><sub><b>hellgy</b></sub></a><br /><a href="#design-hellgy" title="Design">🎨</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jackseye"><img src="https://avatars.githubusercontent.com/u/27834526?v=4?s=100" width="100px;" alt="jackseye"/><br /><sub><b>jackseye</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=jackseye" title="Documentation">📖</a></td>
|
<td align="center"><a href="https://github.com/jackseye"><img src="https://avatars.githubusercontent.com/u/27834526?v=4?s=100" width="100px;" alt="jackseye"/><br /><sub><b>jackseye</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=jackseye" title="Documentation">📖</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/marckiesel"><img src="https://avatars.githubusercontent.com/u/39653780?v=4?s=100" width="100px;" alt="marckiesel"/><br /><sub><b>marckiesel</b></sub></a><br /><a href="#translation-marckiesel" title="Translation">🌍</a></td>
|
<td align="center"><a href="https://github.com/marckiesel"><img src="https://avatars.githubusercontent.com/u/39653780?v=4?s=100" width="100px;" alt="marckiesel"/><br /><sub><b>marckiesel</b></sub></a><br /><a href="#translation-marckiesel" title="Translation">🌍</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Mesil"><img src="https://avatars.githubusercontent.com/u/14284175?v=4?s=100" width="100px;" alt="mesil"/><br /><sub><b>mesil</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/issues?q=author%3Amesil" title="Bug reports">🐛</a></td>
|
<td align="center"><a href="https://github.com/Mesil"><img src="https://avatars.githubusercontent.com/u/14284175?v=4?s=100" width="100px;" alt="mesil"/><br /><sub><b>mesil</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/issues?q=author%3Amesil" title="Bug reports">🐛</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/starfetch"><img src="https://avatars.githubusercontent.com/u/80041179?v=4?s=100" width="100px;" alt="starfetch"/><br /><sub><b>starfetch</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=starfetch" title="Code">💻</a> <a href="https://github.com/freesewing/freesewing/commits?author=starfetch" title="Documentation">📖</a> <a href="#translation-starfetch" title="Translation">🌍</a> <a href="#design-starfetch" title="Design">🎨</a></td>
|
<td align="center"><a href="https://github.com/starfetch"><img src="https://avatars.githubusercontent.com/u/80041179?v=4?s=100" width="100px;" alt="starfetch"/><br /><sub><b>starfetch</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=starfetch" title="Code">💻</a> <a href="https://github.com/freesewing/freesewing/commits?author=starfetch" title="Documentation">📖</a> <a href="#translation-starfetch" title="Translation">🌍</a> <a href="#design-starfetch" title="Design">🎨</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/timorl"><img src="https://avatars.githubusercontent.com/u/4363804?v=4?s=100" width="100px;" alt="timorl"/><br /><sub><b>timorl</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=timorl" title="Code">💻</a></td>
|
<td align="center"><a href="https://github.com/timorl"><img src="https://avatars.githubusercontent.com/u/4363804?v=4?s=100" width="100px;" alt="timorl"/><br /><sub><b>timorl</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=timorl" title="Code">💻</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ttimearl"><img src="https://avatars.githubusercontent.com/u/77916590?v=4?s=100" width="100px;" alt="ttimearl"/><br /><sub><b>ttimearl</b></sub></a><br /><a href="#content-ttimearl" title="Content">🖋</a></td>
|
<td align="center"><a href="https://github.com/ttimearl"><img src="https://avatars.githubusercontent.com/u/77916590?v=4?s=100" width="100px;" alt="ttimearl"/><br /><sub><b>ttimearl</b></sub></a><br /><a href="#content-ttimearl" title="Content">🖋</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/chrisgloom"><img src="https://avatars.githubusercontent.com/u/15905991?v=4?s=100" width="100px;" alt="tuesgloomsday"/><br /><sub><b>tuesgloomsday</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=chrisgloom" title="Documentation">📖</a></td>
|
<td align="center"><a href="https://github.com/chrisgloom"><img src="https://avatars.githubusercontent.com/u/15905991?v=4?s=100" width="100px;" alt="tuesgloomsday"/><br /><sub><b>tuesgloomsday</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=chrisgloom" title="Documentation">📖</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/valadaptive"><img src="https://avatars.githubusercontent.com/u/79560998?v=4?s=100" width="100px;" alt="valadaptive"/><br /><sub><b>valadaptive</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=valadaptive" title="Code">💻</a></td>
|
<td align="center"><a href="https://github.com/valadaptive"><img src="https://avatars.githubusercontent.com/u/79560998?v=4?s=100" width="100px;" alt="valadaptive"/><br /><sub><b>valadaptive</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=valadaptive" title="Code">💻</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/viocky"><img src="https://avatars.githubusercontent.com/u/39279173?v=4?s=100" width="100px;" alt="viocky"/><br /><sub><b>viocky</b></sub></a><br /><a href="#translation-viocky" title="Translation">🌍</a></td>
|
<td align="center"><a href="https://github.com/viocky"><img src="https://avatars.githubusercontent.com/u/39279173?v=4?s=100" width="100px;" alt="viocky"/><br /><sub><b>viocky</b></sub></a><br /><a href="#translation-viocky" title="Translation">🌍</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/woolishboy"><img src="https://avatars.githubusercontent.com/u/57816321?v=4?s=100" width="100px;" alt="woolishboy"/><br /><sub><b>woolishboy</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=woolishboy" title="Code">💻</a></td>
|
<td align="center"><a href="https://github.com/woolishboy"><img src="https://avatars.githubusercontent.com/u/57816321?v=4?s=100" width="100px;" alt="woolishboy"/><br /><sub><b>woolishboy</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=woolishboy" title="Code">💻</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cloutiy"><img src="https://avatars.githubusercontent.com/u/8433147?v=4?s=100" width="100px;" alt="yc"/><br /><sub><b>yc</b></sub></a><br /><a href="#translation-cloutiy" title="Translation">🌍</a></td>
|
<td align="center"><a href="https://github.com/cloutiy"><img src="https://avatars.githubusercontent.com/u/8433147?v=4?s=100" width="100px;" alt="yc"/><br /><sub><b>yc</b></sub></a><br /><a href="#translation-cloutiy" title="Translation">🌍</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { designs, plugins, packages } from './software/index.mjs'
|
||||||
* order. This file takes care of that
|
* order. This file takes care of that
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const first = ['core', 'config-helpers', 'remark-jargon', 'snapseries']
|
const first = ['core', 'remark-jargon', 'snapseries']
|
||||||
const blocks = ['brian', 'titan', 'bella', 'breanna']
|
const blocks = ['brian', 'titan', 'bella', 'breanna']
|
||||||
const extended = ['bent', 'simon', 'carlton', 'ursula']
|
const extended = ['bent', 'simon', 'carlton', 'ursula']
|
||||||
const last = ['i18n']
|
const last = ['i18n']
|
||||||
|
|
|
@ -3,7 +3,6 @@ _types:
|
||||||
peer:
|
peer:
|
||||||
'@freesewing/core': &freesewing '^{{version}}'
|
'@freesewing/core': &freesewing '^{{version}}'
|
||||||
'@freesewing/plugin-bundle': *freesewing
|
'@freesewing/plugin-bundle': *freesewing
|
||||||
'@freesewing/config-helpers': *freesewing
|
|
||||||
dev:
|
dev:
|
||||||
'mocha': &mocha '^10.0.0'
|
'mocha': &mocha '^10.0.0'
|
||||||
'chai': &chai '^4.2.0'
|
'chai': &chai '^4.2.0'
|
||||||
|
@ -50,6 +49,7 @@ charlie:
|
||||||
'@freesewing/plugin-bartack': *freesewing
|
'@freesewing/plugin-bartack': *freesewing
|
||||||
'@freesewing/plugin-mirror': *freesewing
|
'@freesewing/plugin-mirror': *freesewing
|
||||||
'@freesewing/titan': *freesewing
|
'@freesewing/titan': *freesewing
|
||||||
|
'@freesewing/snapseries': *freesewing
|
||||||
core:
|
core:
|
||||||
_:
|
_:
|
||||||
'bezier-js': '^6.1.0'
|
'bezier-js': '^6.1.0'
|
||||||
|
@ -111,6 +111,7 @@ legend:
|
||||||
paco:
|
paco:
|
||||||
peer:
|
peer:
|
||||||
'@freesewing/titan': *freesewing
|
'@freesewing/titan': *freesewing
|
||||||
|
'@freesewing/snapseries': *freesewing
|
||||||
plugin-bundle:
|
plugin-bundle:
|
||||||
dev:
|
dev:
|
||||||
'@freesewing/plugin-banner': *freesewing
|
'@freesewing/plugin-banner': *freesewing
|
||||||
|
@ -154,6 +155,10 @@ rehype-jargon:
|
||||||
rehype-highlight-lines:
|
rehype-highlight-lines:
|
||||||
_:
|
_:
|
||||||
'unist-util-remove': '^3.1.0'
|
'unist-util-remove': '^3.1.0'
|
||||||
|
sandy:
|
||||||
|
'@freesewing/snapseries': *freesewing
|
||||||
|
shin:
|
||||||
|
'@freesewing/snapseries': *freesewing
|
||||||
simon:
|
simon:
|
||||||
peer:
|
peer:
|
||||||
'@freesewing/brian': *freesewing
|
'@freesewing/brian': *freesewing
|
||||||
|
@ -176,6 +181,10 @@ teagan:
|
||||||
peer:
|
peer:
|
||||||
'@freesewing/brian': *freesewing
|
'@freesewing/brian': *freesewing
|
||||||
'@freesewing/plugin-bust': *freesewing
|
'@freesewing/plugin-bust': *freesewing
|
||||||
|
titan:
|
||||||
|
'@freesewing/snapseries': *freesewing
|
||||||
|
trayvon:
|
||||||
|
'@freesewing/snapseries': *freesewing
|
||||||
wahid:
|
wahid:
|
||||||
peer:
|
peer:
|
||||||
'@freesewing/brian': *freesewing
|
'@freesewing/brian': *freesewing
|
||||||
|
|
|
@ -8,13 +8,6 @@ core:
|
||||||
- patterns
|
- patterns
|
||||||
- sewing
|
- sewing
|
||||||
- sewing patterns
|
- sewing patterns
|
||||||
components:
|
|
||||||
- react
|
|
||||||
css-theme:
|
|
||||||
- css
|
|
||||||
- scss
|
|
||||||
- sass
|
|
||||||
- theme
|
|
||||||
design:
|
design:
|
||||||
- design
|
- design
|
||||||
- diy
|
- diy
|
||||||
|
@ -40,10 +33,6 @@ models:
|
||||||
- fashion
|
- fashion
|
||||||
- measurements
|
- measurements
|
||||||
- sizes
|
- sizes
|
||||||
mui-theme:
|
|
||||||
- material-ui
|
|
||||||
- react
|
|
||||||
- theme
|
|
||||||
other:
|
other:
|
||||||
- design
|
- design
|
||||||
- diy
|
- diy
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
{
|
{
|
||||||
"snapseries": "A FreeSewing package to facilitate snapped percentage options in designs",
|
|
||||||
"core": "A library for creating made-to-measure sewing patterns",
|
"core": "A library for creating made-to-measure sewing patterns",
|
||||||
"i18n": "Translations for the FreeSewing project",
|
"i18n": "Translations for the FreeSewing project",
|
||||||
"models": "Body measurements data for a range of default sizes",
|
"models": "Body measurements data for a range of default sizes",
|
||||||
|
|
|
@ -13,6 +13,8 @@ import { version } from '../data.mjs'
|
||||||
import { __loadPatternDefaults } from './config.mjs'
|
import { __loadPatternDefaults } from './config.mjs'
|
||||||
import cloneDeep from 'lodash.clonedeep'
|
import cloneDeep from 'lodash.clonedeep'
|
||||||
|
|
||||||
|
const DISTANCE_DEBUG = false
|
||||||
|
|
||||||
//////////////////////////////////////////////
|
//////////////////////////////////////////////
|
||||||
// CONSTRUCTOR //
|
// CONSTRUCTOR //
|
||||||
//////////////////////////////////////////////
|
//////////////////////////////////////////////
|
||||||
|
@ -474,11 +476,22 @@ Pattern.prototype.__addPartOptions = function (part) {
|
||||||
// Keep design parts immutable in the pattern or risk subtle bugs
|
// Keep design parts immutable in the pattern or risk subtle bugs
|
||||||
this.config.options[optionName] = Object.freeze(part.options[optionName])
|
this.config.options[optionName] = Object.freeze(part.options[optionName])
|
||||||
this.store.log.debug(`🔵 __${optionName}__ option loaded from part \`${part.name}\``)
|
this.store.log.debug(`🔵 __${optionName}__ option loaded from part \`${part.name}\``)
|
||||||
} else if (
|
} else {
|
||||||
this.__mutated.optionDistance[optionName] < this.__mutated.partDistance[part.name]
|
if (DISTANCE_DEBUG)
|
||||||
) {
|
this.store.log.debug(
|
||||||
this.config.options[optionName] = part.options[optionName]
|
'optionDistance for ' +
|
||||||
this.store.log.debug(`🟣 __${optionName}__ option overwritten by \`${part.name}\``)
|
optionName +
|
||||||
|
' is ' +
|
||||||
|
this.__mutated.optionDistance[optionName] +
|
||||||
|
', and partDistance for ' +
|
||||||
|
part.name +
|
||||||
|
' is ' +
|
||||||
|
this.__mutated.partDistance[part.name]
|
||||||
|
)
|
||||||
|
if (this.__mutated.optionDistance[optionName] > this.__mutated.partDistance[part.name]) {
|
||||||
|
this.config.options[optionName] = part.options[optionName]
|
||||||
|
this.store.log.debug(`🟣 __${optionName}__ option overwritten by \`${part.name}\``)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1256,41 +1269,104 @@ Pattern.prototype.__resolveParts = function (count = 0, distance = 0) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
distance++
|
distance++
|
||||||
|
if (DISTANCE_DEBUG) this.store.log.debug('Distance incremented to ' + distance)
|
||||||
for (const part of this.designConfig.parts) {
|
for (const part of this.designConfig.parts) {
|
||||||
if (typeof this.__mutated.partDistance[part.name] === 'undefined')
|
if (typeof this.__mutated.partDistance[part.name] === 'undefined') {
|
||||||
this.__mutated.partDistance[part.name] = distance
|
this.__mutated.partDistance[part.name] = distance
|
||||||
|
if (DISTANCE_DEBUG)
|
||||||
|
this.store.log.debug(
|
||||||
|
'Base partDistance for ' + part.name + ' is ' + this.__mutated.partDistance[part.name]
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for (const [name, part] of Object.entries(this.__designParts)) {
|
for (const [name, part] of Object.entries(this.__designParts)) {
|
||||||
|
const current_part_distance = this.__mutated.partDistance[part.name]
|
||||||
|
const proposed_dependent_part_distance = current_part_distance + 1
|
||||||
// Hide when hideAll is set
|
// Hide when hideAll is set
|
||||||
if (part.hideAll) this.__mutated.partHide[part.name] = true
|
if (part.hideAll) this.__mutated.partHide[part.name] = true
|
||||||
// Inject (from)
|
// Inject (from)
|
||||||
if (part.from) {
|
if (part.from) {
|
||||||
|
if (DISTANCE_DEBUG) this.store.log.debug('Processing ' + part.name + ' "from:"')
|
||||||
this.__setFromHide(part, name, part.from.name)
|
this.__setFromHide(part, name, part.from.name)
|
||||||
this.__designParts[part.from.name] = part.from
|
this.__designParts[part.from.name] = part.from
|
||||||
this.__inject[name] = part.from.name
|
this.__inject[name] = part.from.name
|
||||||
this.__mutated.partDistance[part.from.name] = distance
|
if (
|
||||||
|
typeof this.__mutated.partDistance[part.from.name] === 'undefined' ||
|
||||||
|
this.__mutated.partDistance[part.from.name] < proposed_dependent_part_distance
|
||||||
|
) {
|
||||||
|
this.__mutated.partDistance[part.from.name] = proposed_dependent_part_distance
|
||||||
|
if (DISTANCE_DEBUG)
|
||||||
|
this.store.log.debug(
|
||||||
|
'"from:" partDistance for ' +
|
||||||
|
part.from.name +
|
||||||
|
' is ' +
|
||||||
|
this.__mutated.partDistance[part.from.name]
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Simple dependency (after)
|
// Simple dependency (after)
|
||||||
if (part.after) {
|
if (part.after) {
|
||||||
|
if (DISTANCE_DEBUG) this.store.log.debug('Processing ' + part.name + ' "after:"')
|
||||||
if (Array.isArray(part.after)) {
|
if (Array.isArray(part.after)) {
|
||||||
for (const dep of part.after) {
|
for (const dep of part.after) {
|
||||||
this.__setAfterHide(part, name, dep.name)
|
this.__setAfterHide(part, name, dep.name)
|
||||||
this.__mutated.partDistance[dep.name] = distance
|
|
||||||
this.__designParts[dep.name] = dep
|
this.__designParts[dep.name] = dep
|
||||||
this.__addDependency(name, part, dep)
|
this.__addDependency(name, part, dep)
|
||||||
|
if (
|
||||||
|
typeof this.__mutated.partDistance[dep.name] === 'undefined' ||
|
||||||
|
this.__mutated.partDistance[dep.name] < proposed_dependent_part_distance
|
||||||
|
) {
|
||||||
|
this.__mutated.partDistance[dep.name] = proposed_dependent_part_distance
|
||||||
|
if (DISTANCE_DEBUG)
|
||||||
|
this.store.log.debug(
|
||||||
|
'"after:" partDistance for ' +
|
||||||
|
dep.name +
|
||||||
|
' is ' +
|
||||||
|
this.__mutated.partDistance[dep.name]
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.__setAfterHide(part, name, part.after.name)
|
this.__setAfterHide(part, name, part.after.name)
|
||||||
this.__mutated.partDistance[part.after.name] = distance
|
|
||||||
this.__designParts[part.after.name] = part.after
|
this.__designParts[part.after.name] = part.after
|
||||||
this.__addDependency(name, part, part.after)
|
this.__addDependency(name, part, part.after)
|
||||||
|
if (
|
||||||
|
typeof this.__mutated.partDistance[part.after.name] === 'undefined' ||
|
||||||
|
this.__mutated.partDistance[part.after.name] < proposed_dependent_part_distance
|
||||||
|
) {
|
||||||
|
this.__mutated.partDistance[part.after.name] = proposed_dependent_part_distance
|
||||||
|
if (DISTANCE_DEBUG)
|
||||||
|
this.store.log.debug(
|
||||||
|
'"after:" partDistance for ' +
|
||||||
|
part.after.name +
|
||||||
|
' is ' +
|
||||||
|
this.__mutated.partDistance[part.after.name]
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Did we discover any new dependencies?
|
// Did we discover any new dependencies?
|
||||||
const len = Object.keys(this.__designParts).length
|
const len = Object.keys(this.__designParts).length
|
||||||
// If so, resolve recursively
|
// If so, resolve recursively
|
||||||
if (len > count) return this.__resolveParts(len, distance)
|
if (len > count) {
|
||||||
|
if (DISTANCE_DEBUG) this.store.log.debug('Recursing...')
|
||||||
|
return this.__resolveParts(len, distance)
|
||||||
|
}
|
||||||
|
// Print final part distances.
|
||||||
|
for (const part of this.designConfig.parts) {
|
||||||
|
let qualifier = ''
|
||||||
|
if (DISTANCE_DEBUG) qualifier = 'final '
|
||||||
|
this.store.log.debug(
|
||||||
|
'⚪️ `' +
|
||||||
|
part.name +
|
||||||
|
'` ' +
|
||||||
|
qualifier +
|
||||||
|
'options priority is __' +
|
||||||
|
this.__mutated.partDistance[part.name] +
|
||||||
|
'__'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
for (const part of Object.values(this.__designParts)) this.__addPartConfig(part)
|
for (const part of Object.values(this.__designParts)) this.__addPartConfig(part)
|
||||||
|
|
||||||
|
|
|
@ -631,7 +631,7 @@ export function __asNumber(value, param, method, log) {
|
||||||
value = Number(value)
|
value = Number(value)
|
||||||
return value
|
return value
|
||||||
} catch {
|
} catch {
|
||||||
this.log.error(
|
log.error(
|
||||||
`Called \`${method}(${param})\` but \`${param}\` is not a number nor can it be cast to one`
|
`Called \`${method}(${param})\` but \`${param}\` is not a number nor can it be cast to one`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,7 +63,6 @@ describe('Part', () => {
|
||||||
const part = {
|
const part = {
|
||||||
name: 'test',
|
name: 'test',
|
||||||
draft: ({ getId, part }) => {
|
draft: ({ getId, part }) => {
|
||||||
console.log(getId)
|
|
||||||
id = getId()
|
id = getId()
|
||||||
id = getId()
|
id = getId()
|
||||||
id = getId()
|
id = getId()
|
||||||
|
|
|
@ -1,289 +1,171 @@
|
||||||
import chai from 'chai'
|
import chai from 'chai'
|
||||||
import { round, Path, Point, Design } from '../src/index.mjs'
|
import { round, Path, Point } from '../src/index.mjs'
|
||||||
|
import { pathsProxy } from '../src/path.mjs'
|
||||||
|
|
||||||
const expect = chai.expect
|
const expect = chai.expect
|
||||||
|
|
||||||
describe('Path', () => {
|
describe('Path', () => {
|
||||||
it('Should draw a smurve', () => {
|
describe('smurve', () => {
|
||||||
const part = {
|
it('Should draw a smurve', () => {
|
||||||
name: 'test',
|
const points = {}
|
||||||
draft: ({ Point, points, Path, paths, part }) => {
|
points.from = new Point(10, 20)
|
||||||
points.from = new Point(10, 20)
|
points.cp1 = new Point(40, 10)
|
||||||
points.cp1 = new Point(40, 10)
|
points.cp2 = new Point(60, 30)
|
||||||
points.cp2 = new Point(60, 30)
|
points.to = new Point(90, 20)
|
||||||
points.to = new Point(90, 20)
|
points.scp2 = new Point(140, 10)
|
||||||
points.scp2 = new Point(140, 10)
|
points.sto = new Point(170, 20)
|
||||||
points.sto = new Point(170, 20)
|
|
||||||
|
|
||||||
paths.test = new Path()
|
const test = new Path()
|
||||||
.move(points.from)
|
.move(points.from)
|
||||||
.curve(points.cp1, points.cp2, points.to)
|
.curve(points.cp1, points.cp2, points.to)
|
||||||
.smurve(points.scp2, points.sto)
|
.smurve(points.scp2, points.sto)
|
||||||
|
|
||||||
return part
|
expect(round(test.ops[2].cp1.x)).to.equal(120)
|
||||||
},
|
expect(round(test.ops[2].cp1.y)).to.equal(10)
|
||||||
}
|
})
|
||||||
const design = new Design({ parts: [part] })
|
|
||||||
const pattern = new design()
|
|
||||||
pattern.draft().render()
|
|
||||||
|
|
||||||
expect(round(pattern.parts[0].test.paths.test.ops[2].cp1.x)).to.equal(120)
|
it('Should draw a smurve_', () => {
|
||||||
expect(round(pattern.parts[0].test.paths.test.ops[2].cp1.y)).to.equal(10)
|
const points = {}
|
||||||
})
|
points.from = new Point(10, 20)
|
||||||
|
points.cp1 = new Point(40, 10)
|
||||||
|
points.cp2 = new Point(60, 30)
|
||||||
|
points.to = new Point(90, 20)
|
||||||
|
points.sto = new Point(170, 20)
|
||||||
|
|
||||||
it('Should draw a smurve_', () => {
|
const test = new Path()
|
||||||
const part = {
|
.move(points.from)
|
||||||
name: 'test',
|
.curve(points.cp1, points.cp2, points.to)
|
||||||
draft: ({ Point, points, Path, paths, part }) => {
|
.smurve_(points.sto)
|
||||||
points.from = new Point(10, 20)
|
|
||||||
points.cp1 = new Point(40, 10)
|
|
||||||
points.cp2 = new Point(60, 30)
|
|
||||||
points.to = new Point(90, 20)
|
|
||||||
points.sto = new Point(170, 20)
|
|
||||||
|
|
||||||
paths.test = new Path()
|
expect(round(test.ops[2].cp1.x)).to.equal(120)
|
||||||
.move(points.from)
|
expect(round(test.ops[2].cp1.y)).to.equal(10)
|
||||||
.curve(points.cp1, points.cp2, points.to)
|
})
|
||||||
.smurve_(points.sto)
|
|
||||||
|
|
||||||
return part
|
it('Should log a warning when passing a non-Point to smurve()', () => {
|
||||||
},
|
const points = {}
|
||||||
}
|
points.from = new Point(10, 20)
|
||||||
const design = new Design({ parts: [part] })
|
points.cp1 = new Point(40, 10)
|
||||||
const pattern = new design()
|
points.cp2 = new Point(60, 30)
|
||||||
pattern.draft().render()
|
points.to = new Point(90, 20)
|
||||||
|
|
||||||
expect(round(pattern.parts[0].test.paths.test.ops[2].cp1.x)).to.equal(120)
|
const messages = []
|
||||||
expect(round(pattern.parts[0].test.paths.test.ops[2].cp1.y)).to.equal(10)
|
const log = { warning: (msg) => messages.push(msg) }
|
||||||
})
|
new Path()
|
||||||
|
.__withLog(log)
|
||||||
|
.move(points.from)
|
||||||
|
.curve(points.cp1, points.cp2, points.to)
|
||||||
|
.smurve('hi', 'there')
|
||||||
|
|
||||||
it('Should log a warning when passing a non-Point to smurve()', () => {
|
expect(messages.length).to.equal(2)
|
||||||
const part = {
|
expect(messages[0]).to.equal('Called `Path.smurve(cp2, to)` but `to` is not a `Point` object')
|
||||||
name: 'test',
|
})
|
||||||
draft: ({ Point, points, Path, paths, part }) => {
|
|
||||||
points.from = new Point(10, 20)
|
|
||||||
points.cp1 = new Point(40, 10)
|
|
||||||
points.cp2 = new Point(60, 30)
|
|
||||||
points.to = new Point(90, 20)
|
|
||||||
|
|
||||||
paths.test = new Path()
|
it('Should log a warning when passing a non-Point to smurve_()', () => {
|
||||||
.move(points.from)
|
const messages = []
|
||||||
.curve(points.cp1, points.cp2, points.to)
|
const log = { warning: (msg) => messages.push(msg) }
|
||||||
.smurve('hi', 'there')
|
try {
|
||||||
|
new Path().__withLog(log).smurve_('hi')
|
||||||
return part
|
} catch (e) {
|
||||||
},
|
expect('' + e).to.contain("TypeError: Cannot read properties of undefined (reading 'cp2')")
|
||||||
}
|
} finally {
|
||||||
const design = new Design({ parts: [part] })
|
expect(messages.length).to.equal(1)
|
||||||
const pattern = new design()
|
expect(messages[0]).to.equal('Called `Path.smurve_(to)` but `to` is not a `Point` object')
|
||||||
pattern.draft()
|
}
|
||||||
expect(pattern.setStores[0].logs.warning.length).to.equal(2)
|
})
|
||||||
expect(pattern.setStores[0].logs.warning[0]).to.equal(
|
|
||||||
'Called `Path.smurve(cp2, to)` but `to` is not a `Point` object'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should log a warning when passing a non-Point to smurve_()', () => {
|
|
||||||
const part = {
|
|
||||||
name: 'test',
|
|
||||||
draft: ({ Path, paths, part }) => {
|
|
||||||
paths.test = new Path().smurve_('hi')
|
|
||||||
|
|
||||||
return part
|
|
||||||
},
|
|
||||||
}
|
|
||||||
const design = new Design({ parts: [part] })
|
|
||||||
const pattern = new design()
|
|
||||||
pattern.draft()
|
|
||||||
expect(pattern.setStores[0].logs.warning.length).to.equal(1)
|
|
||||||
expect(pattern.setStores[0].logs.warning[0]).to.equal(
|
|
||||||
'Called `Path.smurve_(to)` but `to` is not a `Point` object'
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should log a warning when passing a non-Path to the paths proxy', () => {
|
it('Should log a warning when passing a non-Path to the paths proxy', () => {
|
||||||
const part = {
|
const messages = []
|
||||||
name: 'test',
|
const log = { warning: (msg) => messages.push(msg) }
|
||||||
draft: ({ paths, part }) => {
|
const pathsObj = {}
|
||||||
paths.test = 'Wriing code can get very lonely sometimes'
|
const paths = pathsProxy(pathsObj, log)
|
||||||
|
paths.set(pathsObj, 'test', 'Writing code can get very lonely sometimes')
|
||||||
|
|
||||||
return part
|
expect(messages.length).to.equal(2)
|
||||||
},
|
expect(messages[0]).to.equal('`paths.test` was set with a value that is not a `Path` object')
|
||||||
}
|
expect(messages[1]).to.equal('Could not set `name` property on `paths.test`')
|
||||||
const design = new Design({ parts: [part] })
|
|
||||||
const pattern = new design()
|
|
||||||
pattern.draft()
|
|
||||||
expect(pattern.setStores[0].logs.warning.length).to.equal(2)
|
|
||||||
expect(pattern.setStores[0].logs.warning[0]).to.equal(
|
|
||||||
'`paths.test` was set with a value that is not a `Path` object'
|
|
||||||
)
|
|
||||||
expect(pattern.setStores[0].logs.warning[1]).to.equal(
|
|
||||||
'Could not set `name` property on `paths.test`'
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should offset a line', () => {
|
describe('offset', () => {
|
||||||
const part = {
|
it('Should offset a line', () => {
|
||||||
name: 'test',
|
const line = new Path().move(new Point(0, 0)).line(new Point(0, 40))
|
||||||
draft: ({ paths, Path, Point, part }) => {
|
const offLine = line.offset(10)
|
||||||
paths.line = new Path().move(new Point(0, 0)).line(new Point(0, 40))
|
const bbox = offLine.bbox()
|
||||||
paths.offset = paths.line.offset(10)
|
expect(bbox.bottomRight.x).to.equal(-10)
|
||||||
return part
|
expect(bbox.bottomRight.y).to.equal(40)
|
||||||
},
|
})
|
||||||
}
|
|
||||||
const design = new Design({ parts: [part] })
|
it('Should offset a curve', () => {
|
||||||
const pattern = new design()
|
const curve = new Path()
|
||||||
pattern.draft().render()
|
.move(new Point(0, 0))
|
||||||
expect(pattern.parts[0].test.paths.offset.bottomRight.x).to.equal(-10)
|
.curve(new Point(0, 40), new Point(123, 34), new Point(23, 4))
|
||||||
expect(pattern.parts[0].test.paths.offset.bottomRight.y).to.equal(40)
|
const offset = curve.offset(10)
|
||||||
|
const bbox = offset.bbox()
|
||||||
|
expect(round(bbox.bottomRight.x)).to.equal(72.18)
|
||||||
|
expect(round(bbox.bottomRight.y)).to.equal(38.26)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should offset a curve where cp1 = start', () => {
|
||||||
|
const curve = new Path().move(new Point(0, 0))._curve(new Point(123, 34), new Point(23, 4))
|
||||||
|
const offset = curve.offset(10)
|
||||||
|
const bbox = offset.bbox()
|
||||||
|
expect(round(bbox.bottomRight.x)).to.equal(72.63)
|
||||||
|
expect(round(bbox.bottomRight.y)).to.equal(26.47)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should offset a curve where cp2 = end', () => {
|
||||||
|
const curve = new Path().move(new Point(0, 0)).curve_(new Point(40, 0), new Point(123, 34))
|
||||||
|
const offset = curve.offset(10)
|
||||||
|
const bbox = offset.bbox()
|
||||||
|
expect(round(bbox.bottomRight.x)).to.equal(119.86)
|
||||||
|
expect(round(bbox.bottomRight.y)).to.equal(43.49)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should offset a curve', () => {
|
describe('length', () => {
|
||||||
const part = {
|
it('Should return the length of a line', () => {
|
||||||
name: 'test',
|
const line = new Path().move(new Point(0, 0)).line(new Point(40, 0))
|
||||||
draft: ({ paths, Path, Point, part }) => {
|
expect(line.length()).to.equal(40)
|
||||||
paths.curve = new Path()
|
})
|
||||||
.move(new Point(0, 0))
|
|
||||||
.curve(new Point(0, 40), new Point(123, 34), new Point(23, 4))
|
|
||||||
paths.offset = paths.curve.offset(10)
|
|
||||||
return part
|
|
||||||
},
|
|
||||||
}
|
|
||||||
const design = new Design({ parts: [part] })
|
|
||||||
const pattern = new design()
|
|
||||||
pattern.draft().render()
|
|
||||||
expect(round(pattern.parts[0].test.paths.offset.bottomRight.x)).to.equal(72.18)
|
|
||||||
expect(round(pattern.parts[0].test.paths.offset.bottomRight.y)).to.equal(38.26)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should offset a curve where cp1 = start', () => {
|
it('Should return the length of a curve', () => {
|
||||||
const part = {
|
const curve = new Path()
|
||||||
name: 'test',
|
.move(new Point(0, 0))
|
||||||
draft: ({ paths, Path, Point, part }) => {
|
.curve(new Point(0, 40), new Point(123, 34), new Point(23, 4))
|
||||||
paths.curve = new Path().move(new Point(0, 0))._curve(new Point(123, 34), new Point(23, 4))
|
.close()
|
||||||
paths.offset = paths.curve.offset(10)
|
expect(round(curve.length())).to.equal(145.11)
|
||||||
return part
|
})
|
||||||
},
|
|
||||||
}
|
|
||||||
const design = new Design({ parts: [part] })
|
|
||||||
const pattern = new design()
|
|
||||||
pattern.draft().render()
|
|
||||||
expect(round(pattern.parts[0].test.paths.offset.bottomRight.x)).to.equal(72.63)
|
|
||||||
expect(round(pattern.parts[0].test.paths.offset.bottomRight.y)).to.equal(26.47)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should offset a curve where cp2 = end', () => {
|
|
||||||
const part = {
|
|
||||||
name: 'test',
|
|
||||||
draft: ({ paths, Path, Point, part }) => {
|
|
||||||
paths.curve = new Path().move(new Point(0, 0)).curve_(new Point(40, 0), new Point(123, 34))
|
|
||||||
paths.offset = paths.curve.offset(10)
|
|
||||||
return part
|
|
||||||
},
|
|
||||||
}
|
|
||||||
const design = new Design({ parts: [part] })
|
|
||||||
const pattern = new design()
|
|
||||||
pattern.draft().render()
|
|
||||||
expect(round(pattern.parts[0].test.paths.offset.bottomRight.x)).to.equal(119.86)
|
|
||||||
expect(round(pattern.parts[0].test.paths.offset.bottomRight.y)).to.equal(43.49)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should return the length of a line', () => {
|
|
||||||
const part = {
|
|
||||||
name: 'test',
|
|
||||||
draft: ({ paths, Path, Point, part }) => {
|
|
||||||
paths.line = new Path().move(new Point(0, 0)).line(new Point(40, 0))
|
|
||||||
return part
|
|
||||||
},
|
|
||||||
}
|
|
||||||
const design = new Design({ parts: [part] })
|
|
||||||
const pattern = new design()
|
|
||||||
pattern.draft().render()
|
|
||||||
expect(pattern.parts[0].test.paths.line.length()).to.equal(40)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should return the length of a curve', () => {
|
|
||||||
const part = {
|
|
||||||
name: 'test',
|
|
||||||
draft: ({ paths, Path, Point, part }) => {
|
|
||||||
paths.curve = new Path()
|
|
||||||
.move(new Point(0, 0))
|
|
||||||
.curve(new Point(0, 40), new Point(123, 34), new Point(23, 4))
|
|
||||||
.close()
|
|
||||||
return part
|
|
||||||
},
|
|
||||||
}
|
|
||||||
const design = new Design({ parts: [part] })
|
|
||||||
const pattern = new design()
|
|
||||||
pattern.draft().render()
|
|
||||||
expect(round(pattern.parts[0].test.paths.curve.length())).to.equal(145.11)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should return the rough length of a curve', () => {
|
it('Should return the rough length of a curve', () => {
|
||||||
const part = {
|
const curve = new Path()
|
||||||
name: 'test',
|
.move(new Point(0, 0))
|
||||||
draft: ({ paths, Path, Point, part }) => {
|
.curve(new Point(0, 50), new Point(100, 50), new Point(100, 0))
|
||||||
paths.curve = new Path()
|
.close()
|
||||||
.move(new Point(0, 0))
|
expect(round(curve.roughLength())).to.equal(300)
|
||||||
.curve(new Point(0, 50), new Point(100, 50), new Point(100, 0))
|
|
||||||
.close()
|
|
||||||
return part
|
|
||||||
},
|
|
||||||
}
|
|
||||||
const design = new Design({ parts: [part] })
|
|
||||||
const pattern = new design()
|
|
||||||
pattern.draft().render()
|
|
||||||
expect(round(pattern.parts[0].test.paths.curve.roughLength())).to.equal(300)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should return the rough length of a line', () => {
|
it('Should return the rough length of a line', () => {
|
||||||
const part = {
|
const line = new Path().move(new Point(0, 0)).line(new Point(0, 50))
|
||||||
name: 'test',
|
expect(round(line.roughLength())).to.equal(50)
|
||||||
draft: ({ paths, Path, Point, part }) => {
|
|
||||||
paths.line = new Path().move(new Point(0, 0)).line(new Point(0, 50))
|
|
||||||
return part
|
|
||||||
},
|
|
||||||
}
|
|
||||||
const design = new Design({ parts: [part] })
|
|
||||||
const pattern = new design()
|
|
||||||
pattern.draft().render()
|
|
||||||
expect(round(pattern.parts[0].test.paths.line.roughLength())).to.equal(50)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should return the path start point', () => {
|
it('Should return the path start point', () => {
|
||||||
const part = {
|
const curve = new Path()
|
||||||
name: 'test',
|
.move(new Point(123, 456))
|
||||||
draft: ({ paths, Path, Point, part }) => {
|
.curve(new Point(0, 40), new Point(123, 34), new Point(23, 4))
|
||||||
paths.curve = new Path()
|
.close()
|
||||||
.move(new Point(123, 456))
|
expect(curve.start().x).to.equal(123)
|
||||||
.curve(new Point(0, 40), new Point(123, 34), new Point(23, 4))
|
expect(curve.start().y).to.equal(456)
|
||||||
.close()
|
|
||||||
return part
|
|
||||||
},
|
|
||||||
}
|
|
||||||
const design = new Design({ parts: [part] })
|
|
||||||
const pattern = new design()
|
|
||||||
pattern.draft().render()
|
|
||||||
expect(pattern.parts[0].test.paths.curve.start().x).to.equal(123)
|
|
||||||
expect(pattern.parts[0].test.paths.curve.start().y).to.equal(456)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should return the path end point', () => {
|
it('Should return the path end point', () => {
|
||||||
const part = {
|
const curve = new Path()
|
||||||
name: 'test',
|
.move(new Point(123, 456))
|
||||||
draft: ({ paths, Path, Point, part }) => {
|
.curve(new Point(0, 40), new Point(123, 34), new Point(23, 4))
|
||||||
paths.curve = new Path()
|
.close()
|
||||||
.move(new Point(123, 456))
|
expect(curve.end().x).to.equal(123)
|
||||||
.curve(new Point(0, 40), new Point(123, 34), new Point(23, 4))
|
expect(curve.end().y).to.equal(456)
|
||||||
.close()
|
|
||||||
return part
|
|
||||||
},
|
|
||||||
}
|
|
||||||
const design = new Design({ parts: [part] })
|
|
||||||
const pattern = new design()
|
|
||||||
pattern.draft().render()
|
|
||||||
expect(pattern.parts[0].test.paths.curve.end().x).to.equal(123)
|
|
||||||
expect(pattern.parts[0].test.paths.curve.end().y).to.equal(456)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should calculate that path boundary', () => {
|
it('Should calculate that path boundary', () => {
|
||||||
|
@ -810,24 +692,17 @@ describe('Path', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should overwrite a path attribute', () => {
|
it('Should overwrite a path attribute', () => {
|
||||||
const part = {
|
const line = new Path()
|
||||||
name: 'test',
|
line.log = { debug: () => {} }
|
||||||
draft: ({ paths, Path, Point, part }) => {
|
line
|
||||||
paths.line = new Path()
|
.move(new Point(0, 0))
|
||||||
.move(new Point(0, 0))
|
.line(new Point(0, 40))
|
||||||
.line(new Point(0, 40))
|
.attr('class', 'foo')
|
||||||
.attr('class', 'foo')
|
.attr('class', 'bar')
|
||||||
.attr('class', 'bar')
|
.attr('class', 'overwritten', true)
|
||||||
.attr('class', 'overwritten', true)
|
|
||||||
return part
|
|
||||||
},
|
|
||||||
}
|
|
||||||
const design = new Design({ parts: [part] })
|
|
||||||
const pattern = new design()
|
|
||||||
pattern.draft().render()
|
|
||||||
|
|
||||||
// Paths from shorthand have the log method
|
// Paths from shorthand have the log method
|
||||||
expect(pattern.parts[0].test.paths.line.attributes.get('class')).to.equal('overwritten')
|
expect(line.attributes.get('class')).to.equal('overwritten')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should move along a path even if it lands just on a joint', () => {
|
it('Should move along a path even if it lands just on a joint', () => {
|
||||||
|
@ -1054,7 +929,6 @@ describe('Path', () => {
|
||||||
it('Should log a warning when setting an attribute without a name', () => {
|
it('Should log a warning when setting an attribute without a name', () => {
|
||||||
let invalid = false
|
let invalid = false
|
||||||
const log = { warning: () => (invalid = true) }
|
const log = { warning: () => (invalid = true) }
|
||||||
expect(invalid).to.equal(false)
|
|
||||||
new Path().__withLog(log).attr()
|
new Path().__withLog(log).attr()
|
||||||
expect(invalid).to.equal(true)
|
expect(invalid).to.equal(true)
|
||||||
})
|
})
|
||||||
|
@ -1062,52 +936,42 @@ describe('Path', () => {
|
||||||
it('Should log a warning when setting an attribute without a value', () => {
|
it('Should log a warning when setting an attribute without a value', () => {
|
||||||
let invalid = false
|
let invalid = false
|
||||||
const log = { warning: () => (invalid = true) }
|
const log = { warning: () => (invalid = true) }
|
||||||
expect(invalid).to.equal(false)
|
|
||||||
new Path().__withLog(log).attr('test')
|
new Path().__withLog(log).attr('test')
|
||||||
expect(invalid).to.equal(true)
|
expect(invalid).to.equal(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should log a warning when calling offset without a distance', () => {
|
it('Should log an error when calling offset without a distance', () => {
|
||||||
const part = {
|
let invalid = true
|
||||||
name: 'test',
|
const log = { warning: () => {}, error: () => (invalid = true) }
|
||||||
draft: ({ paths, Path, Point, points }) => {
|
const pointLog = { error: () => {} }
|
||||||
paths.line = new Path().move(new Point(0, 0)).line(new Point(0, 40)).attr('class', 'foo')
|
const pointA = new Point(0, 0).__withLog(pointLog)
|
||||||
paths.a = new Path().move(points.a).line(points.b)
|
const pointB = new Point(0, 40).__withLog(pointLog)
|
||||||
paths.b = paths.a.offset()
|
const a = new Path().__withLog(log).move(pointA).line(pointB)
|
||||||
return part
|
a.offset()
|
||||||
},
|
expect(invalid).to.equal(true)
|
||||||
}
|
|
||||||
const design = new Design({ parts: [part] })
|
|
||||||
const pattern = new design()
|
|
||||||
pattern.draft()
|
|
||||||
expect(pattern.setStores[0].logs.error.length).to.equal(2)
|
|
||||||
expect(pattern.setStores[0].logs.error[0]).to.equal(
|
|
||||||
'Called `Path.offset(distance)` but `distance` is not a number'
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should log a warning when calling join without a path', () => {
|
it('Should log an error when calling join without a path', () => {
|
||||||
const part = {
|
let invalid = false
|
||||||
name: 'test',
|
const log = { error: () => (invalid = true) }
|
||||||
draft: ({ paths, Path, Point, points }) => {
|
const line = new Path()
|
||||||
paths.line = new Path().move(new Point(0, 0)).line(new Point(0, 40)).attr('class', 'foo')
|
.move(new Point(0, 0))
|
||||||
paths.a = new Path().move(points.a).line(points.b).join()
|
.line(new Point(0, 40))
|
||||||
return part
|
.attr('class', 'foo')
|
||||||
},
|
.__withLog(log)
|
||||||
|
|
||||||
|
try {
|
||||||
|
line.join()
|
||||||
|
} catch (e) {
|
||||||
|
expect('' + e).to.contain("TypeError: Cannot read properties of undefined (reading 'ops')")
|
||||||
|
} finally {
|
||||||
|
expect(invalid).to.equal(true)
|
||||||
}
|
}
|
||||||
const design = new Design({ parts: [part] })
|
|
||||||
const pattern = new design()
|
|
||||||
pattern.draft()
|
|
||||||
expect(pattern.setStores[0].logs.error.length).to.equal(2)
|
|
||||||
expect(pattern.setStores[0].logs.error[0]).to.equal(
|
|
||||||
'Called `Path.join(that)` but `that` is not a `Path` object'
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should log a warning when calling start on a path without drawing operations', () => {
|
it('Should log a warning when calling start on a path without drawing operations', () => {
|
||||||
let invalid = false
|
let invalid = false
|
||||||
const log = { error: () => (invalid = true) }
|
const log = { error: () => (invalid = true) }
|
||||||
expect(invalid).to.equal(false)
|
|
||||||
try {
|
try {
|
||||||
new Path().__withLog(log).start()
|
new Path().__withLog(log).start()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -1116,10 +980,9 @@ describe('Path', () => {
|
||||||
expect(invalid).to.equal(true)
|
expect(invalid).to.equal(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should log a warning when calling end on a path without drawing operations', () => {
|
it('Should log an error when calling end on a path without drawing operations', () => {
|
||||||
let invalid = false
|
let invalid = false
|
||||||
const log = { error: () => (invalid = true) }
|
const log = { error: () => (invalid = true) }
|
||||||
expect(invalid).to.equal(false)
|
|
||||||
try {
|
try {
|
||||||
new Path().__withLog(log).end()
|
new Path().__withLog(log).end()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -1128,7 +991,7 @@ describe('Path', () => {
|
||||||
expect(invalid).to.equal(true)
|
expect(invalid).to.equal(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should log a warning when calling shiftAlong but distance is not a number', () => {
|
it('Should log an error when calling shiftAlong but distance is not a number', () => {
|
||||||
let invalid = false
|
let invalid = false
|
||||||
const log = { error: () => (invalid = true) }
|
const log = { error: () => (invalid = true) }
|
||||||
expect(invalid).to.equal(false)
|
expect(invalid).to.equal(false)
|
||||||
|
@ -1136,53 +999,36 @@ describe('Path', () => {
|
||||||
expect(invalid).to.equal(true)
|
expect(invalid).to.equal(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should log a warning when calling shiftFractionalong but fraction is not a number', () => {
|
it('Should log an error when calling shiftFractionalong but fraction is not a number', () => {
|
||||||
const part = {
|
let invalid = false
|
||||||
name: 'test',
|
const log = { error: () => (invalid = true) }
|
||||||
draft: ({ Path, Point, points }) => {
|
new Path().__withLog(log).move(new Point(0, 0)).line(new Point(0, 40)).shiftFractionAlong()
|
||||||
points.a = new Path().move(new Point(0, 0)).line(new Point(0, 40)).shiftFractionAlong()
|
expect(invalid).to.equal(true)
|
||||||
return part
|
|
||||||
},
|
|
||||||
}
|
|
||||||
const design = new Design({ parts: [part] })
|
|
||||||
const pattern = new design()
|
|
||||||
pattern.draft()
|
|
||||||
expect(pattern.setStores[0].logs.error[0]).to.equal(
|
|
||||||
'Called `Path.shiftFractionAlong(fraction)` but `fraction` is not a number'
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should log a warning when splitting a path on a non-point', () => {
|
it('Should log an error when splitting a path on a non-point', () => {
|
||||||
const part = {
|
let invalid = false
|
||||||
name: 'test',
|
const log = { error: () => (invalid = true) }
|
||||||
draft: ({ Path, Point, points, part }) => {
|
const pointLog = { warning: () => {} }
|
||||||
points.a = new Path().move(new Point(0, 0)).line(new Point(0, 40)).split()
|
try {
|
||||||
return part
|
new Path()
|
||||||
},
|
.__withLog(log)
|
||||||
|
.move(new Point(0, 0).__withLog(pointLog))
|
||||||
|
.line(new Point(0, 40).__withLog(pointLog))
|
||||||
|
.split()
|
||||||
|
} catch (e) {
|
||||||
|
expect(e.toString()).to.include(
|
||||||
|
"TypeError: Cannot read properties of undefined (reading '__check')"
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
expect(invalid).to.equal(true)
|
||||||
}
|
}
|
||||||
const design = new Design({ parts: [part] })
|
|
||||||
const pattern = new design()
|
|
||||||
pattern.draft()
|
|
||||||
expect(pattern.setStores[0].logs.error[0]).to.equal(
|
|
||||||
'Called `Path.split(point)` but `point` is not a `Point` object'
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should add a class', () => {
|
it('Should add a class', () => {
|
||||||
const part = {
|
const line = new Path().move(new Point(0, 0)).line(new Point(10, 10)).addClass('fabric banana')
|
||||||
name: 'test',
|
|
||||||
draft: ({ Path, paths, Point, part }) => {
|
expect(line.attributes.get('class')).to.equal('fabric banana')
|
||||||
paths.line = new Path()
|
|
||||||
.move(new Point(0, 0))
|
|
||||||
.line(new Point(10, 10))
|
|
||||||
.addClass('fabric banana')
|
|
||||||
return part
|
|
||||||
},
|
|
||||||
}
|
|
||||||
const design = new Design({ parts: [part] })
|
|
||||||
const pattern = new design()
|
|
||||||
pattern.draft()
|
|
||||||
expect(pattern.parts[0].test.paths.line.attributes.get('class')).to.equal('fabric banana')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should (un)hide a path with hide()/unhide()', () => {
|
it('Should (un)hide a path with hide()/unhide()', () => {
|
||||||
|
|
|
@ -132,26 +132,30 @@ describe('Point', () => {
|
||||||
expect(round(ss.shiftTowards(se, 200).y)).to.equal(-18.42)
|
expect(round(ss.shiftTowards(se, 200).y)).to.equal(-18.42)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should shift a point a fraction towards another', () => {
|
describe('shiftFractionTowards', () => {
|
||||||
let origin = new Point(0, 0)
|
it('Should shift a point a fraction towards another', () => {
|
||||||
let n = new Point(0, -10)
|
let origin = new Point(0, 0)
|
||||||
let e = new Point(10, 0)
|
let s = new Point(0, 10)
|
||||||
let s = new Point(0, 10)
|
let ss = origin.shiftFractionTowards(s, 0.5)
|
||||||
let w = new Point(-10, 0)
|
expect(round(ss.x)).to.equal(0)
|
||||||
let sn = origin.shiftFractionTowards(n, 1.5)
|
expect(round(ss.y)).to.equal(5)
|
||||||
let se = origin.shiftFractionTowards(e, 1.5)
|
})
|
||||||
let ss = origin.shiftFractionTowards(s, 0.5)
|
|
||||||
let sw = origin.shiftFractionTowards(w, 2.5)
|
it('Should shift a point a fraction beyond another if the fraction is > 1', () => {
|
||||||
expect(round(sn.x)).to.equal(0)
|
let origin = new Point(0, 0)
|
||||||
expect(round(sn.y)).to.equal(-15)
|
let n = new Point(0, -10)
|
||||||
expect(round(se.x)).to.equal(15)
|
let sn = origin.shiftFractionTowards(n, 1.5)
|
||||||
expect(round(se.y)).to.equal(0)
|
expect(round(sn.x)).to.equal(0)
|
||||||
expect(round(ss.x)).to.equal(0)
|
expect(round(sn.y)).to.equal(-15)
|
||||||
expect(round(ss.y)).to.equal(5)
|
})
|
||||||
expect(round(sw.x)).to.equal(-25)
|
|
||||||
expect(round(sw.y)).to.equal(0)
|
it('Should shift a point a fraction away from another if the fraction is < 0', () => {
|
||||||
expect(round(sw.shiftFractionTowards(sn, 100).x)).to.equal(2475)
|
let origin = new Point(0, 0)
|
||||||
expect(round(ss.shiftFractionTowards(se, 200).y)).to.equal(-995)
|
let n = new Point(0, -10)
|
||||||
|
let sn = origin.shiftFractionTowards(n, -0.5)
|
||||||
|
expect(round(sn.x)).to.equal(0)
|
||||||
|
expect(round(sn.y)).to.equal(5)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should shift a point beyond another', () => {
|
it('Should shift a point beyond another', () => {
|
||||||
|
|
|
@ -154,47 +154,49 @@ describe('Utils', () => {
|
||||||
expect(hits.length).to.equal(3)
|
expect(hits.length).to.equal(3)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should find 9 intersections between two curves', () => {
|
describe('curvesIntersect', function () {
|
||||||
let A = new Point(10, 10)
|
it('Should find 9 intersections between two curves', () => {
|
||||||
let Acp = new Point(310, 40)
|
let A = new Point(10, 10)
|
||||||
let B = new Point(110, 70)
|
let Acp = new Point(310, 40)
|
||||||
let Bcp = new Point(-210, 40)
|
let B = new Point(110, 70)
|
||||||
let C = new Point(20, -5)
|
let Bcp = new Point(-210, 40)
|
||||||
let Ccp = new Point(60, 300)
|
let C = new Point(20, -5)
|
||||||
let D = new Point(100, 85)
|
let Ccp = new Point(60, 300)
|
||||||
let Dcp = new Point(70, -220)
|
let D = new Point(100, 85)
|
||||||
|
let Dcp = new Point(70, -220)
|
||||||
|
|
||||||
let hits = curvesIntersect(A, Acp, Bcp, B, C, Ccp, Dcp, D)
|
let hits = curvesIntersect(A, Acp, Bcp, B, C, Ccp, Dcp, D)
|
||||||
expect(hits.length).to.equal(9)
|
expect(hits.length).to.equal(9)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should find 1 intersection between two curves', () => {
|
it('Should find 1 intersection between two curves', () => {
|
||||||
let A = new Point(10, 10)
|
let A = new Point(10, 10)
|
||||||
let Acp = new Point(310, 40)
|
let Acp = new Point(310, 40)
|
||||||
let B = new Point(110, 70)
|
let B = new Point(110, 70)
|
||||||
let Bcp = new Point(-210, 40)
|
let Bcp = new Point(-210, 40)
|
||||||
let C = new Point(20, -5)
|
let C = new Point(20, -5)
|
||||||
let Ccp = new Point(-60, 300)
|
let Ccp = new Point(-60, 300)
|
||||||
let D = new Point(-200, 85)
|
let D = new Point(-200, 85)
|
||||||
let Dcp = new Point(-270, -220)
|
let Dcp = new Point(-270, -220)
|
||||||
|
|
||||||
let hit = curvesIntersect(A, Acp, Bcp, B, C, Ccp, Dcp, D)
|
let hit = curvesIntersect(A, Acp, Bcp, B, C, Ccp, Dcp, D)
|
||||||
expect(round(hit.x)).to.equal(15.58)
|
expect(round(hit.x)).to.equal(15.58)
|
||||||
expect(round(hit.y)).to.equal(10.56)
|
expect(round(hit.y)).to.equal(10.56)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should find no intersection between two curves', () => {
|
it('Should find no intersection between two curves', () => {
|
||||||
let A = new Point(10, 10)
|
let A = new Point(10, 10)
|
||||||
let Acp = new Point(310, 40)
|
let Acp = new Point(310, 40)
|
||||||
let B = new Point(110, 70)
|
let B = new Point(110, 70)
|
||||||
let Bcp = new Point(-210, 40)
|
let Bcp = new Point(-210, 40)
|
||||||
let C = new Point(20, -5)
|
let C = new Point(20, -5)
|
||||||
let Ccp = new Point(-60, -300)
|
let Ccp = new Point(-60, -300)
|
||||||
let D = new Point(-200, 85)
|
let D = new Point(-200, 85)
|
||||||
let Dcp = new Point(-270, -220)
|
let Dcp = new Point(-270, -220)
|
||||||
|
|
||||||
let hit = curvesIntersect(A, Acp, Bcp, B, C, Ccp, Dcp, D)
|
let hit = curvesIntersect(A, Acp, Bcp, B, C, Ccp, Dcp, D)
|
||||||
expect(hit).to.equal(false)
|
expect(hit).to.equal(false)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should correctly format units', () => {
|
it('Should correctly format units', () => {
|
||||||
|
|
|
@ -1,25 +1,37 @@
|
||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
const path = require('path');
|
const path = require('path')
|
||||||
const spawn = require('child_process').spawn
|
const spawn = require('child_process').spawn
|
||||||
|
|
||||||
const projectRoot = path.normalize(path.join(__dirname, '..'));
|
const projectRoot = path.normalize(path.join(__dirname, '..'))
|
||||||
const outputLog = path.join(projectRoot, '.test-failures.log');
|
const outputLog = path.join(projectRoot, '.test-failures.log')
|
||||||
const collectorScript = path.join(projectRoot, 'scripts', 'test-failure-collector.js');
|
const collectorScript = path.join(projectRoot, 'scripts', 'test-failure-collector.js')
|
||||||
|
|
||||||
// Start with a fresh output log on each run.
|
// Start with a fresh output log on each run.
|
||||||
if (fs.existsSync(outputLog)) {
|
if (fs.existsSync(outputLog)) {
|
||||||
fs.unlinkSync(outputLog);
|
fs.unlinkSync(outputLog)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run all tests, specifying the collector script.
|
// Run all tests, specifying the collector script.
|
||||||
spawn('lerna', ['run', '--no-bail', 'testci', '--stream', '--parallel', '--loglevel', 'error', '--', '--file', `${collectorScript}`, '--no-warnings'], { stdio: 'inherit' })
|
spawn(
|
||||||
.on('exit', function(code) {
|
'lerna',
|
||||||
// If a failure occurred, the log file will have been created. Print it.
|
[
|
||||||
if (fs.existsSync(outputLog)) {
|
'run',
|
||||||
console.error(fs.readFileSync(outputLog, 'utf8').trim());
|
'--no-bail',
|
||||||
}
|
'testci',
|
||||||
|
'--loglevel',
|
||||||
// Propagate the exit code.
|
'error',
|
||||||
process.exit(code);
|
'--',
|
||||||
});
|
'--file',
|
||||||
|
`${collectorScript}`,
|
||||||
|
'--no-warnings',
|
||||||
|
],
|
||||||
|
{ stdio: 'inherit' }
|
||||||
|
).on('exit', function (code) {
|
||||||
|
// If a failure occurred, the log file will have been created. Print it.
|
||||||
|
if (fs.existsSync(outputLog)) {
|
||||||
|
console.error(fs.readFileSync(outputLog, 'utf8').trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Propagate the exit code.
|
||||||
|
process.exit(code)
|
||||||
|
})
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
"author": "Joost De Cock <joost@joost.at> (https://github.com/joostdecock)",
|
"author": "Joost De Cock <joost@joost.at> (https://github.com/joostdecock)",
|
||||||
"homepage": "https://freesewing.org/",
|
"homepage": "https://freesewing.org/",
|
||||||
"repository": "github:freesewing/freesewing",
|
"repository": "github:freesewing/freesewing",
|
||||||
"license": "MIT",
|
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/freesewing/freesewing/issues"
|
"url": "https://github.com/freesewing/freesewing/issues"
|
||||||
},
|
},
|
||||||
|
@ -30,10 +29,13 @@
|
||||||
"crypto": "^1.0.1",
|
"crypto": "^1.0.1",
|
||||||
"express": "4.18.2",
|
"express": "4.18.2",
|
||||||
"mustache": "^4.2.0",
|
"mustache": "^4.2.0",
|
||||||
|
"passport": "^0.6.0",
|
||||||
"passport-http": "^0.3.0",
|
"passport-http": "^0.3.0",
|
||||||
|
"passport-jwt": "^4.0.0",
|
||||||
"pino": "^8.7.0"
|
"pino": "^8.7.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"chai-http": "^4.3.0",
|
||||||
"mocha": "^10.1.0",
|
"mocha": "^10.1.0",
|
||||||
"mocha-steps": "^1.3.0",
|
"mocha-steps": "^1.3.0",
|
||||||
"prisma": "4.5.0"
|
"prisma": "4.5.0"
|
||||||
|
|
|
@ -73,9 +73,12 @@ model Pattern {
|
||||||
data String
|
data String
|
||||||
design String
|
design String
|
||||||
img String?
|
img String?
|
||||||
|
name String @default("")
|
||||||
|
notes String
|
||||||
person Person? @relation(fields: [personId], references: [id])
|
person Person? @relation(fields: [personId], references: [id])
|
||||||
personId Int?
|
personId Int?
|
||||||
notes String
|
public Boolean @default(false)
|
||||||
|
settings String
|
||||||
user User @relation(fields: [userId], references: [id])
|
user User @relation(fields: [userId], references: [id])
|
||||||
userId Int
|
userId Int
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
Binary file not shown.
|
@ -4,7 +4,7 @@ import { log } from '../utils/log.mjs'
|
||||||
import { ApikeyModel } from '../models/apikey.mjs'
|
import { ApikeyModel } from '../models/apikey.mjs'
|
||||||
import { UserModel } from '../models/user.mjs'
|
import { UserModel } from '../models/user.mjs'
|
||||||
|
|
||||||
export function ApikeyController() {}
|
export function ApikeysController() {}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Create API key
|
* Create API key
|
||||||
|
@ -12,7 +12,7 @@ export function ApikeyController() {}
|
||||||
* This is the endpoint that handles creation of API keys/tokens
|
* This is the endpoint that handles creation of API keys/tokens
|
||||||
* See: https://freesewing.dev/reference/backend/api/apikey
|
* See: https://freesewing.dev/reference/backend/api/apikey
|
||||||
*/
|
*/
|
||||||
ApikeyController.prototype.create = async (req, res, tools) => {
|
ApikeysController.prototype.create = async (req, res, tools) => {
|
||||||
const Apikey = new ApikeyModel(tools)
|
const Apikey = new ApikeyModel(tools)
|
||||||
await Apikey.create(req)
|
await Apikey.create(req)
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ ApikeyController.prototype.create = async (req, res, tools) => {
|
||||||
* This is the endpoint that handles creation of API keys/tokens
|
* This is the endpoint that handles creation of API keys/tokens
|
||||||
* See: https://freesewing.dev/reference/backend/api/apikey
|
* See: https://freesewing.dev/reference/backend/api/apikey
|
||||||
*/
|
*/
|
||||||
ApikeyController.prototype.read = async (req, res, tools) => {
|
ApikeysController.prototype.read = async (req, res, tools) => {
|
||||||
const Apikey = new ApikeyModel(tools)
|
const Apikey = new ApikeyModel(tools)
|
||||||
await Apikey.guardedRead(req)
|
await Apikey.guardedRead(req)
|
||||||
|
|
||||||
|
@ -39,7 +39,7 @@ ApikeyController.prototype.read = async (req, res, tools) => {
|
||||||
* request
|
* request
|
||||||
* See: https://freesewing.dev/reference/backend/api/apikey
|
* See: https://freesewing.dev/reference/backend/api/apikey
|
||||||
*/
|
*/
|
||||||
ApikeyController.prototype.whoami = async (req, res, tools) => {
|
ApikeysController.prototype.whoami = async (req, res, tools) => {
|
||||||
const User = new UserModel(tools)
|
const User = new UserModel(tools)
|
||||||
const Apikey = new ApikeyModel(tools)
|
const Apikey = new ApikeyModel(tools)
|
||||||
|
|
||||||
|
@ -69,7 +69,7 @@ ApikeyController.prototype.whoami = async (req, res, tools) => {
|
||||||
* This is the endpoint that handles removal of API keys/tokens
|
* This is the endpoint that handles removal of API keys/tokens
|
||||||
* See: https://freesewing.dev/reference/backend/api/apikey
|
* See: https://freesewing.dev/reference/backend/api/apikey
|
||||||
*/
|
*/
|
||||||
ApikeyController.prototype.delete = async (req, res, tools) => {
|
ApikeysController.prototype.delete = async (req, res, tools) => {
|
||||||
const Apikey = new ApikeyModel(tools)
|
const Apikey = new ApikeyModel(tools)
|
||||||
await Apikey.guardedDelete(req)
|
await Apikey.guardedDelete(req)
|
||||||
|
|
58
sites/backend/src/controllers/patterns.mjs
Normal file
58
sites/backend/src/controllers/patterns.mjs
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
import { PatternModel } from '../models/pattern.mjs'
|
||||||
|
|
||||||
|
export function PatternsController() {}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Create a pattern
|
||||||
|
* See: https://freesewing.dev/reference/backend/api
|
||||||
|
*/
|
||||||
|
PatternsController.prototype.create = async (req, res, tools) => {
|
||||||
|
const Pattern = new PatternModel(tools)
|
||||||
|
await Pattern.guardedCreate(req)
|
||||||
|
|
||||||
|
return Pattern.sendResponse(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Read a pattern
|
||||||
|
* See: https://freesewing.dev/reference/backend/api
|
||||||
|
*/
|
||||||
|
PatternsController.prototype.read = async (req, res, tools) => {
|
||||||
|
const Pattern = new PatternModel(tools)
|
||||||
|
await Pattern.guardedRead(req)
|
||||||
|
|
||||||
|
return Pattern.sendResponse(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Update a pattern
|
||||||
|
* See: https://freesewing.dev/reference/backend/api
|
||||||
|
*/
|
||||||
|
PatternsController.prototype.update = async (req, res, tools) => {
|
||||||
|
const Pattern = new PatternModel(tools)
|
||||||
|
await Pattern.guardedUpdate(req)
|
||||||
|
|
||||||
|
return Pattern.sendResponse(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Remove a pattern
|
||||||
|
* See: https://freesewing.dev/reference/backend/api
|
||||||
|
*/
|
||||||
|
PatternsController.prototype.delete = async (req, res, tools) => {
|
||||||
|
const Pattern = new PatternModel(tools)
|
||||||
|
await Pattern.guardedDelete(req)
|
||||||
|
|
||||||
|
return Pattern.sendResponse(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Clone a pattern
|
||||||
|
* See: https://freesewing.dev/reference/backend/api
|
||||||
|
*/
|
||||||
|
PatternsController.prototype.clone = async (req, res, tools) => {
|
||||||
|
const Pattern = new PatternModel(tools)
|
||||||
|
await Pattern.guardedClone(req)
|
||||||
|
|
||||||
|
return Pattern.sendResponse(res)
|
||||||
|
}
|
|
@ -1,12 +1,12 @@
|
||||||
import { PersonModel } from '../models/person.mjs'
|
import { PersonModel } from '../models/person.mjs'
|
||||||
|
|
||||||
export function PersonController() {}
|
export function PeopleController() {}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Create a person for the authenticated user
|
* Create a person for the authenticated user
|
||||||
* See: https://freesewing.dev/reference/backend/api
|
* See: https://freesewing.dev/reference/backend/api
|
||||||
*/
|
*/
|
||||||
PersonController.prototype.create = async (req, res, tools) => {
|
PeopleController.prototype.create = async (req, res, tools) => {
|
||||||
const Person = new PersonModel(tools)
|
const Person = new PersonModel(tools)
|
||||||
await Person.guardedCreate(req)
|
await Person.guardedCreate(req)
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ PersonController.prototype.create = async (req, res, tools) => {
|
||||||
* Read a person
|
* Read a person
|
||||||
* See: https://freesewing.dev/reference/backend/api
|
* See: https://freesewing.dev/reference/backend/api
|
||||||
*/
|
*/
|
||||||
PersonController.prototype.read = async (req, res, tools) => {
|
PeopleController.prototype.read = async (req, res, tools) => {
|
||||||
const Person = new PersonModel(tools)
|
const Person = new PersonModel(tools)
|
||||||
await Person.guardedRead(req)
|
await Person.guardedRead(req)
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@ PersonController.prototype.read = async (req, res, tools) => {
|
||||||
* Update a person
|
* Update a person
|
||||||
* See: https://freesewing.dev/reference/backend/api
|
* See: https://freesewing.dev/reference/backend/api
|
||||||
*/
|
*/
|
||||||
PersonController.prototype.update = async (req, res, tools) => {
|
PeopleController.prototype.update = async (req, res, tools) => {
|
||||||
const Person = new PersonModel(tools)
|
const Person = new PersonModel(tools)
|
||||||
await Person.guardedUpdate(req)
|
await Person.guardedUpdate(req)
|
||||||
|
|
||||||
|
@ -39,7 +39,7 @@ PersonController.prototype.update = async (req, res, tools) => {
|
||||||
* Remove a person
|
* Remove a person
|
||||||
* See: https://freesewing.dev/reference/backend/api
|
* See: https://freesewing.dev/reference/backend/api
|
||||||
*/
|
*/
|
||||||
PersonController.prototype.delete = async (req, res, tools) => {
|
PeopleController.prototype.delete = async (req, res, tools) => {
|
||||||
const Person = new PersonModel(tools)
|
const Person = new PersonModel(tools)
|
||||||
await Person.guardedDelete(req)
|
await Person.guardedDelete(req)
|
||||||
|
|
||||||
|
@ -50,7 +50,7 @@ PersonController.prototype.delete = async (req, res, tools) => {
|
||||||
* Clone a person
|
* Clone a person
|
||||||
* See: https://freesewing.dev/reference/backend/api
|
* See: https://freesewing.dev/reference/backend/api
|
||||||
*/
|
*/
|
||||||
PersonController.prototype.clone = async (req, res, tools) => {
|
PeopleController.prototype.clone = async (req, res, tools) => {
|
||||||
const Person = new PersonModel(tools)
|
const Person = new PersonModel(tools)
|
||||||
await Person.guardedClone(req)
|
await Person.guardedClone(req)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { UserModel } from '../models/user.mjs'
|
import { UserModel } from '../models/user.mjs'
|
||||||
|
|
||||||
export function UserController() {}
|
export function UsersController() {}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Signup
|
* Signup
|
||||||
|
@ -8,7 +8,7 @@ export function UserController() {}
|
||||||
* This is the endpoint that handles account signups
|
* This is the endpoint that handles account signups
|
||||||
* See: https://freesewing.dev/reference/backend/api
|
* See: https://freesewing.dev/reference/backend/api
|
||||||
*/
|
*/
|
||||||
UserController.prototype.signup = async (req, res, tools) => {
|
UsersController.prototype.signup = async (req, res, tools) => {
|
||||||
const User = new UserModel(tools)
|
const User = new UserModel(tools)
|
||||||
await User.guardedCreate(req)
|
await User.guardedCreate(req)
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ UserController.prototype.signup = async (req, res, tools) => {
|
||||||
* This is the endpoint that fully unlocks the account if the user gives their consent
|
* This is the endpoint that fully unlocks the account if the user gives their consent
|
||||||
* See: https://freesewing.dev/reference/backend/api
|
* See: https://freesewing.dev/reference/backend/api
|
||||||
*/
|
*/
|
||||||
UserController.prototype.confirm = async (req, res, tools) => {
|
UsersController.prototype.confirm = async (req, res, tools) => {
|
||||||
const User = new UserModel(tools)
|
const User = new UserModel(tools)
|
||||||
await User.confirm(req)
|
await User.confirm(req)
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ UserController.prototype.confirm = async (req, res, tools) => {
|
||||||
* This is the endpoint that provides traditional username/password login
|
* This is the endpoint that provides traditional username/password login
|
||||||
* See: https://freesewing.dev/reference/backend/api
|
* See: https://freesewing.dev/reference/backend/api
|
||||||
*/
|
*/
|
||||||
UserController.prototype.login = async function (req, res, tools) {
|
UsersController.prototype.login = async function (req, res, tools) {
|
||||||
const User = new UserModel(tools)
|
const User = new UserModel(tools)
|
||||||
await User.passwordLogin(req)
|
await User.passwordLogin(req)
|
||||||
|
|
||||||
|
@ -46,7 +46,7 @@ UserController.prototype.login = async function (req, res, tools) {
|
||||||
*
|
*
|
||||||
* See: https://freesewing.dev/reference/backend/api
|
* See: https://freesewing.dev/reference/backend/api
|
||||||
*/
|
*/
|
||||||
UserController.prototype.whoami = async (req, res, tools) => {
|
UsersController.prototype.whoami = async (req, res, tools) => {
|
||||||
const User = new UserModel(tools)
|
const User = new UserModel(tools)
|
||||||
await User.guardedRead({ id: req.user.uid }, req)
|
await User.guardedRead({ id: req.user.uid }, req)
|
||||||
|
|
||||||
|
@ -58,7 +58,7 @@ UserController.prototype.whoami = async (req, res, tools) => {
|
||||||
*
|
*
|
||||||
* See: https://freesewing.dev/reference/backend/api
|
* See: https://freesewing.dev/reference/backend/api
|
||||||
*/
|
*/
|
||||||
UserController.prototype.update = async (req, res, tools) => {
|
UsersController.prototype.update = async (req, res, tools) => {
|
||||||
const User = new UserModel(tools)
|
const User = new UserModel(tools)
|
||||||
await User.guardedRead({ id: req.user.uid }, req)
|
await User.guardedRead({ id: req.user.uid }, req)
|
||||||
await User.guardedUpdate(req)
|
await User.guardedUpdate(req)
|
317
sites/backend/src/models/pattern.mjs
Normal file
317
sites/backend/src/models/pattern.mjs
Normal file
|
@ -0,0 +1,317 @@
|
||||||
|
import { log } from '../utils/log.mjs'
|
||||||
|
import { setPatternAvatar } from '../utils/sanity.mjs'
|
||||||
|
|
||||||
|
export function PatternModel(tools) {
|
||||||
|
this.config = tools.config
|
||||||
|
this.prisma = tools.prisma
|
||||||
|
this.decrypt = tools.decrypt
|
||||||
|
this.encrypt = tools.encrypt
|
||||||
|
this.encryptedFields = ['data', 'img', 'name', 'notes', 'settings']
|
||||||
|
this.clear = {} // For holding decrypted data
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
data String
|
||||||
|
design String
|
||||||
|
img String?
|
||||||
|
person Person? @relation(fields: [personId], references: [id])
|
||||||
|
personId Int?
|
||||||
|
name String @default("")
|
||||||
|
notes String
|
||||||
|
public
|
||||||
|
settings String
|
||||||
|
user User @relation(fields: [userId], references: [id])
|
||||||
|
userId Int
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
*/
|
||||||
|
|
||||||
|
PatternModel.prototype.guardedCreate = async function ({ body, user }) {
|
||||||
|
if (user.level < 3) return this.setResponse(403, 'insufficientAccessLevel')
|
||||||
|
if (Object.keys(body) < 2) return this.setResponse(400, 'postBodyMissing')
|
||||||
|
if (!body.person) return this.setResponse(400, 'personMissing')
|
||||||
|
if (typeof body.person !== 'number') return this.setResponse(400, 'personNotNumeric')
|
||||||
|
if (typeof body.settings !== 'object') return this.setResponse(400, 'settingsNotAnObject')
|
||||||
|
if (body.data && typeof body.data !== 'object') return this.setResponse(400, 'dataNotAnObject')
|
||||||
|
if (!body.design && !body.data?.design) return this.setResponse(400, 'designMissing')
|
||||||
|
if (typeof body.design !== 'string') return this.setResponse(400, 'designNotStringy')
|
||||||
|
|
||||||
|
// Prepare data
|
||||||
|
const data = {
|
||||||
|
design: body.design,
|
||||||
|
personId: body.person,
|
||||||
|
settings: body.settings,
|
||||||
|
}
|
||||||
|
// Data (will be encrypted, so always set _some_ value)
|
||||||
|
if (typeof body.data === 'object') data.data = body.data
|
||||||
|
else data.data = {}
|
||||||
|
// Name (will be encrypted, so always set _some_ value)
|
||||||
|
if (typeof body.name === 'string' && body.name.length > 0) data.name = body.name
|
||||||
|
else data.name = '--'
|
||||||
|
// Notes (will be encrypted, so always set _some_ value)
|
||||||
|
if (typeof body.notes === 'string' && body.notes.length > 0) data.notes = body.notes
|
||||||
|
else data.notes = '--'
|
||||||
|
// Public
|
||||||
|
if (body.public === true) data.public = true
|
||||||
|
data.userId = user.uid
|
||||||
|
// Set this one initially as we need the ID to create a custom img via Sanity
|
||||||
|
data.img = this.config.avatars.pattern
|
||||||
|
|
||||||
|
// Create record
|
||||||
|
await this.unguardedCreate(data)
|
||||||
|
|
||||||
|
// Update img? (now that we have the ID)
|
||||||
|
const img =
|
||||||
|
this.config.use.sanity &&
|
||||||
|
typeof body.img === 'string' &&
|
||||||
|
(!body.unittest || (body.unittest && this.config.use.tests?.sanity))
|
||||||
|
? await setPatternAvatar(this.record.id, body.img)
|
||||||
|
: false
|
||||||
|
|
||||||
|
if (img) await this.unguardedUpdate(this.cloak({ img: img.url }))
|
||||||
|
else await this.read({ id: this.record.id })
|
||||||
|
|
||||||
|
return this.setResponse(201, 'created', { pattern: this.asPattern() })
|
||||||
|
}
|
||||||
|
|
||||||
|
PatternModel.prototype.unguardedCreate = async function (data) {
|
||||||
|
try {
|
||||||
|
this.record = await this.prisma.pattern.create({ data: this.cloak(data) })
|
||||||
|
} catch (err) {
|
||||||
|
log.warn(err, 'Could not create pattern')
|
||||||
|
return this.setResponse(500, 'createPatternFailed')
|
||||||
|
}
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Loads a pattern from the database based on the where clause you pass it
|
||||||
|
*
|
||||||
|
* Stores result in this.record
|
||||||
|
*/
|
||||||
|
PatternModel.prototype.read = async function (where) {
|
||||||
|
try {
|
||||||
|
this.record = await this.prisma.pattern.findUnique({ where })
|
||||||
|
} catch (err) {
|
||||||
|
log.warn({ err, where }, 'Could not read pattern')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.reveal()
|
||||||
|
|
||||||
|
return this.setExists()
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Loads a pattern from the database based on the where clause you pass it
|
||||||
|
* In addition prepares it for returning the pattern data
|
||||||
|
*
|
||||||
|
* Stores result in this.record
|
||||||
|
*/
|
||||||
|
PatternModel.prototype.guardedRead = async function ({ params, user }) {
|
||||||
|
if (user.level < 1) return this.setResponse(403, 'insufficientAccessLevel')
|
||||||
|
if (user.iss && user.status < 1) return this.setResponse(403, 'accountStatusLacking')
|
||||||
|
|
||||||
|
await this.read({ id: parseInt(params.id) })
|
||||||
|
if (this.record.userId !== user.uid && user.level < 5) {
|
||||||
|
return this.setResponse(403, 'insufficientAccessLevel')
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.setResponse(200, false, {
|
||||||
|
result: 'success',
|
||||||
|
pattern: this.asPattern(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Clones a pattern
|
||||||
|
* In addition prepares it for returning the pattern data
|
||||||
|
*
|
||||||
|
* Stores result in this.record
|
||||||
|
*/
|
||||||
|
PatternModel.prototype.guardedClone = async function ({ params, user }) {
|
||||||
|
if (user.level < 3) return this.setResponse(403, 'insufficientAccessLevel')
|
||||||
|
if (user.iss && user.status < 1) return this.setResponse(403, 'accountStatusLacking')
|
||||||
|
|
||||||
|
await this.read({ id: parseInt(params.id) })
|
||||||
|
if (this.record.userId !== user.uid && !this.record.public && user.level < 5) {
|
||||||
|
return this.setResponse(403, 'insufficientAccessLevel')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clone pattern
|
||||||
|
const data = this.asPattern()
|
||||||
|
delete data.id
|
||||||
|
data.name += ` (cloned from #${this.record.id})`
|
||||||
|
data.notes += ` (Note: This pattern was cloned from pattern #${this.record.id})`
|
||||||
|
await this.unguardedCreate(data)
|
||||||
|
|
||||||
|
// Update unencrypted data
|
||||||
|
this.reveal()
|
||||||
|
|
||||||
|
return this.setResponse(200, false, {
|
||||||
|
result: 'success',
|
||||||
|
pattern: this.asPattern(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Helper method to decrypt at-rest data
|
||||||
|
*/
|
||||||
|
PatternModel.prototype.reveal = async function () {
|
||||||
|
this.clear = {}
|
||||||
|
if (this.record) {
|
||||||
|
for (const field of this.encryptedFields) {
|
||||||
|
this.clear[field] = this.decrypt(this.record[field])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Helper method to encrypt at-rest data
|
||||||
|
*/
|
||||||
|
PatternModel.prototype.cloak = function (data) {
|
||||||
|
for (const field of this.encryptedFields) {
|
||||||
|
if (typeof data[field] !== 'undefined') {
|
||||||
|
data[field] = this.encrypt(data[field])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Checks this.record and sets a boolean to indicate whether
|
||||||
|
* the pattern exists or not
|
||||||
|
*
|
||||||
|
* Stores result in this.exists
|
||||||
|
*/
|
||||||
|
PatternModel.prototype.setExists = function () {
|
||||||
|
this.exists = this.record ? true : false
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Updates the pattern data - Used when we create the data ourselves
|
||||||
|
* so we know it's safe
|
||||||
|
*/
|
||||||
|
PatternModel.prototype.unguardedUpdate = async function (data) {
|
||||||
|
try {
|
||||||
|
this.record = await this.prisma.pattern.update({
|
||||||
|
where: { id: this.record.id },
|
||||||
|
data,
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
log.warn(err, 'Could not update pattern record')
|
||||||
|
process.exit()
|
||||||
|
return this.setResponse(500, 'updatePatternFailed')
|
||||||
|
}
|
||||||
|
await this.reveal()
|
||||||
|
|
||||||
|
return this.setResponse(200)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Updates the pattern data - Used when we pass through user-provided data
|
||||||
|
* so we can't be certain it's safe
|
||||||
|
*/
|
||||||
|
PatternModel.prototype.guardedUpdate = async function ({ params, body, user }) {
|
||||||
|
if (user.level < 3) return this.setResponse(403, 'insufficientAccessLevel')
|
||||||
|
if (user.iss && user.status < 1) return this.setResponse(403, 'accountStatusLacking')
|
||||||
|
await this.read({ id: parseInt(params.id) })
|
||||||
|
if (this.record.userId !== user.uid && user.level < 8) {
|
||||||
|
return this.setResponse(403, 'insufficientAccessLevel')
|
||||||
|
}
|
||||||
|
const data = {}
|
||||||
|
// Name
|
||||||
|
if (typeof body.name === 'string') data.name = body.name
|
||||||
|
// Notes
|
||||||
|
if (typeof body.notes === 'string') data.notes = body.notes
|
||||||
|
// Public
|
||||||
|
if (body.public === true || body.public === false) data.public = body.public
|
||||||
|
// Data
|
||||||
|
if (typeof body.data === 'object') data.data = body.data
|
||||||
|
// Settings
|
||||||
|
if (typeof body.settings === 'object') data.settings = body.settings
|
||||||
|
// Image (img)
|
||||||
|
if (typeof body.img === 'string') {
|
||||||
|
const img = await setPatternAvatar(params.id, body.img)
|
||||||
|
data.img = img.url
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now update the record
|
||||||
|
await this.unguardedUpdate(this.cloak(data))
|
||||||
|
|
||||||
|
return this.setResponse(200, false, { pattern: this.asPattern() })
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Removes the pattern - No questions asked
|
||||||
|
*/
|
||||||
|
PatternModel.prototype.unguardedDelete = async function () {
|
||||||
|
await this.prisma.pattern.delete({ here: { id: this.record.id } })
|
||||||
|
this.record = null
|
||||||
|
this.clear = null
|
||||||
|
|
||||||
|
return this.setExists()
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Removes the pattern - Checks permissions
|
||||||
|
*/
|
||||||
|
PatternModel.prototype.guardedDelete = async function ({ params, body, user }) {
|
||||||
|
if (user.level < 3) return this.setResponse(403, 'insufficientAccessLevel')
|
||||||
|
if (user.iss && user.status < 1) return this.setResponse(403, 'accountStatusLacking')
|
||||||
|
|
||||||
|
await this.read({ id: parseInt(params.id) })
|
||||||
|
if (this.record.userId !== user.uid && user.level < 8) {
|
||||||
|
return this.setResponse(403, 'insufficientAccessLevel')
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.unguardedDelete()
|
||||||
|
|
||||||
|
return this.setResponse(204, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Returns record data
|
||||||
|
*/
|
||||||
|
PatternModel.prototype.asPattern = function () {
|
||||||
|
return {
|
||||||
|
...this.record,
|
||||||
|
...this.clear,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Helper method to set the response code, result, and body
|
||||||
|
*
|
||||||
|
* Will be used by this.sendResponse()
|
||||||
|
*/
|
||||||
|
PatternModel.prototype.setResponse = function (status = 200, error = false, data = {}) {
|
||||||
|
this.response = {
|
||||||
|
status,
|
||||||
|
body: {
|
||||||
|
result: 'success',
|
||||||
|
...data,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if (status > 201) {
|
||||||
|
this.response.body.error = error
|
||||||
|
this.response.body.result = 'error'
|
||||||
|
this.error = true
|
||||||
|
} else this.error = false
|
||||||
|
|
||||||
|
return this.setExists()
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Helper method to send response
|
||||||
|
*/
|
||||||
|
PatternModel.prototype.sendResponse = async function (res) {
|
||||||
|
return res.status(this.response.status).send(this.response.body)
|
||||||
|
}
|
|
@ -19,7 +19,12 @@ PersonModel.prototype.guardedCreate = async function ({ body, user }) {
|
||||||
|
|
||||||
// Prepare data
|
// Prepare data
|
||||||
const data = { name: body.name }
|
const data = { name: body.name }
|
||||||
|
// Name (will be encrypted, so always set _some_ value)
|
||||||
|
if (typeof body.name === 'string') data.name = body.name
|
||||||
|
else data.name = '--'
|
||||||
|
// Notes (will be encrypted, so always set _some_ value)
|
||||||
if (body.notes || typeof body.notes === 'string') data.notes = body.notes
|
if (body.notes || typeof body.notes === 'string') data.notes = body.notes
|
||||||
|
else data.notes = '--'
|
||||||
if (body.public === true) data.public = true
|
if (body.public === true) data.public = true
|
||||||
if (body.measies) data.measies = this.sanitizeMeasurements(body.measies)
|
if (body.measies) data.measies = this.sanitizeMeasurements(body.measies)
|
||||||
data.imperial = body.imperial === true ? true : false
|
data.imperial = body.imperial === true ? true : false
|
||||||
|
|
|
@ -223,9 +223,9 @@ UserModel.prototype.guardedCreate = async function ({ body }) {
|
||||||
* Login based on username + password
|
* Login based on username + password
|
||||||
*/
|
*/
|
||||||
UserModel.prototype.passwordLogin = async function (req) {
|
UserModel.prototype.passwordLogin = async function (req) {
|
||||||
if (Object.keys(req.body) < 1) return this.setReponse(400, 'postBodyMissing')
|
if (Object.keys(req.body) < 1) return this.setResponse(400, 'postBodyMissing')
|
||||||
if (!req.body.username) return this.setReponse(400, 'usernameMissing')
|
if (!req.body.username) return this.setResponse(400, 'usernameMissing')
|
||||||
if (!req.body.password) return this.setReponse(400, 'passwordMissing')
|
if (!req.body.password) return this.setResponse(400, 'passwordMissing')
|
||||||
|
|
||||||
await this.find(req.body)
|
await this.find(req.body)
|
||||||
if (!this.exists) {
|
if (!this.exists) {
|
||||||
|
@ -255,7 +255,7 @@ UserModel.prototype.passwordLogin = async function (req) {
|
||||||
* Confirms a user account
|
* Confirms a user account
|
||||||
*/
|
*/
|
||||||
UserModel.prototype.confirm = async function ({ body, params }) {
|
UserModel.prototype.confirm = async function ({ body, params }) {
|
||||||
if (!params.id) return this.setReponse(404, 'missingConfirmationId')
|
if (!params.id) return this.setResponse(404, 'missingConfirmationId')
|
||||||
if (Object.keys(body) < 1) return this.setResponse(400, 'postBodyMissing')
|
if (Object.keys(body) < 1) return this.setResponse(400, 'postBodyMissing')
|
||||||
if (!body.consent || typeof body.consent !== 'number' || body.consent < 1)
|
if (!body.consent || typeof body.consent !== 'number' || body.consent < 1)
|
||||||
return this.setResponse(400, 'consentRequired')
|
return this.setResponse(400, 'consentRequired')
|
||||||
|
|
|
@ -1,38 +0,0 @@
|
||||||
import { ApikeyController } from '../controllers/apikey.mjs'
|
|
||||||
|
|
||||||
const Apikey = new ApikeyController()
|
|
||||||
const jwt = ['jwt', { session: false }]
|
|
||||||
const bsc = ['basic', { session: false }]
|
|
||||||
|
|
||||||
export function apikeyRoutes(tools) {
|
|
||||||
const { app, passport } = tools
|
|
||||||
|
|
||||||
// Create Apikey
|
|
||||||
app.post('/apikey/jwt', passport.authenticate(...jwt), (req, res) =>
|
|
||||||
Apikey.create(req, res, tools)
|
|
||||||
)
|
|
||||||
app.post('/apikey/key', passport.authenticate(...bsc), (req, res) =>
|
|
||||||
Apikey.create(req, res, tools)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Read Apikey
|
|
||||||
app.get('/apikey/:id/jwt', passport.authenticate(...jwt), (req, res) =>
|
|
||||||
Apikey.read(req, res, tools)
|
|
||||||
)
|
|
||||||
app.get('/apikey/:id/key', passport.authenticate(...bsc), (req, res) =>
|
|
||||||
Apikey.read(req, res, tools)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Read current Apikey
|
|
||||||
app.get('/whoami/key', passport.authenticate(...bsc), (req, res) =>
|
|
||||||
Apikey.whoami(req, res, tools)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Remove Apikey
|
|
||||||
app.delete('/apikey/:id/jwt', passport.authenticate(...jwt), (req, res) =>
|
|
||||||
Apikey.delete(req, res, tools)
|
|
||||||
)
|
|
||||||
app.delete('/apikey/:id/key', passport.authenticate(...bsc), (req, res) =>
|
|
||||||
Apikey.delete(req, res, tools)
|
|
||||||
)
|
|
||||||
}
|
|
38
sites/backend/src/routes/apikeys.mjs
Normal file
38
sites/backend/src/routes/apikeys.mjs
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import { ApikeysController } from '../controllers/apikeys.mjs'
|
||||||
|
|
||||||
|
const Apikeys = new ApikeysController()
|
||||||
|
const jwt = ['jwt', { session: false }]
|
||||||
|
const bsc = ['basic', { session: false }]
|
||||||
|
|
||||||
|
export function apikeysRoutes(tools) {
|
||||||
|
const { app, passport } = tools
|
||||||
|
|
||||||
|
// Create Apikey
|
||||||
|
app.post('/apikeys/jwt', passport.authenticate(...jwt), (req, res) =>
|
||||||
|
Apikeys.create(req, res, tools)
|
||||||
|
)
|
||||||
|
app.post('/apikeys/key', passport.authenticate(...bsc), (req, res) =>
|
||||||
|
Apikeys.create(req, res, tools)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Read Apikey
|
||||||
|
app.get('/apikeys/:id/jwt', passport.authenticate(...jwt), (req, res) =>
|
||||||
|
Apikeys.read(req, res, tools)
|
||||||
|
)
|
||||||
|
app.get('/apikeys/:id/key', passport.authenticate(...bsc), (req, res) =>
|
||||||
|
Apikeys.read(req, res, tools)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Read current Apikey
|
||||||
|
app.get('/whoami/key', passport.authenticate(...bsc), (req, res) =>
|
||||||
|
Apikeys.whoami(req, res, tools)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Remove Apikey
|
||||||
|
app.delete('/apikeys/:id/jwt', passport.authenticate(...jwt), (req, res) =>
|
||||||
|
Apikeys.delete(req, res, tools)
|
||||||
|
)
|
||||||
|
app.delete('/apikeys/:id/key', passport.authenticate(...bsc), (req, res) =>
|
||||||
|
Apikeys.delete(req, res, tools)
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,9 +1,11 @@
|
||||||
import { apikeyRoutes } from './apikey.mjs'
|
import { apikeysRoutes } from './apikeys.mjs'
|
||||||
import { userRoutes } from './user.mjs'
|
import { usersRoutes } from './users.mjs'
|
||||||
import { personRoutes } from './person.mjs'
|
import { peopleRoutes } from './people.mjs'
|
||||||
|
import { patternsRoutes } from './patterns.mjs'
|
||||||
|
|
||||||
export const routes = {
|
export const routes = {
|
||||||
apikeyRoutes,
|
apikeysRoutes,
|
||||||
userRoutes,
|
usersRoutes,
|
||||||
personRoutes,
|
peopleRoutes,
|
||||||
|
patternsRoutes,
|
||||||
}
|
}
|
||||||
|
|
49
sites/backend/src/routes/patterns.mjs
Normal file
49
sites/backend/src/routes/patterns.mjs
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
import { PatternsController } from '../controllers/patterns.mjs'
|
||||||
|
|
||||||
|
const Patterns = new PatternsController()
|
||||||
|
const jwt = ['jwt', { session: false }]
|
||||||
|
const bsc = ['basic', { session: false }]
|
||||||
|
|
||||||
|
export function patternsRoutes(tools) {
|
||||||
|
const { app, passport } = tools
|
||||||
|
|
||||||
|
// Create pattern
|
||||||
|
app.post('/patterns/jwt', passport.authenticate(...jwt), (req, res) =>
|
||||||
|
Patterns.create(req, res, tools)
|
||||||
|
)
|
||||||
|
app.post('/patterns/key', passport.authenticate(...bsc), (req, res) =>
|
||||||
|
Patterns.create(req, res, tools)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Clone pattern
|
||||||
|
app.post('/patterns/:id/clone/jwt', passport.authenticate(...jwt), (req, res) =>
|
||||||
|
Patterns.clone(req, res, tools)
|
||||||
|
)
|
||||||
|
app.post('/patterns/:id/clone/key', passport.authenticate(...bsc), (req, res) =>
|
||||||
|
Patterns.clone(req, res, tools)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Read pattern
|
||||||
|
app.get('/patterns/:id/jwt', passport.authenticate(...jwt), (req, res) =>
|
||||||
|
Patterns.read(req, res, tools)
|
||||||
|
)
|
||||||
|
app.get('/patterns/:id/key', passport.authenticate(...bsc), (req, res) =>
|
||||||
|
Patterns.read(req, res, tools)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Update pattern
|
||||||
|
app.put('/patterns/:id/jwt', passport.authenticate(...jwt), (req, res) =>
|
||||||
|
Patterns.update(req, res, tools)
|
||||||
|
)
|
||||||
|
app.put('/patterns/:id/key', passport.authenticate(...bsc), (req, res) =>
|
||||||
|
Patterns.update(req, res, tools)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Delete pattern
|
||||||
|
app.delete('/patterns/:id/jwt', passport.authenticate(...jwt), (req, res) =>
|
||||||
|
Patterns.delete(req, res, tools)
|
||||||
|
)
|
||||||
|
app.delete('/patterns/:id/key', passport.authenticate(...bsc), (req, res) =>
|
||||||
|
Patterns.delete(req, res, tools)
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,49 +1,49 @@
|
||||||
import { PersonController } from '../controllers/person.mjs'
|
import { PeopleController } from '../controllers/people.mjs'
|
||||||
|
|
||||||
const Person = new PersonController()
|
const People = new PeopleController()
|
||||||
const jwt = ['jwt', { session: false }]
|
const jwt = ['jwt', { session: false }]
|
||||||
const bsc = ['basic', { session: false }]
|
const bsc = ['basic', { session: false }]
|
||||||
|
|
||||||
export function personRoutes(tools) {
|
export function peopleRoutes(tools) {
|
||||||
const { app, passport } = tools
|
const { app, passport } = tools
|
||||||
|
|
||||||
// Create person
|
// Create person
|
||||||
app.post('/people/jwt', passport.authenticate(...jwt), (req, res) =>
|
app.post('/people/jwt', passport.authenticate(...jwt), (req, res) =>
|
||||||
Person.create(req, res, tools)
|
People.create(req, res, tools)
|
||||||
)
|
)
|
||||||
app.post('/people/key', passport.authenticate(...bsc), (req, res) =>
|
app.post('/people/key', passport.authenticate(...bsc), (req, res) =>
|
||||||
Person.create(req, res, tools)
|
People.create(req, res, tools)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Clone person
|
// Clone person
|
||||||
app.post('/people/:id/clone/jwt', passport.authenticate(...jwt), (req, res) =>
|
app.post('/people/:id/clone/jwt', passport.authenticate(...jwt), (req, res) =>
|
||||||
Person.clone(req, res, tools)
|
People.clone(req, res, tools)
|
||||||
)
|
)
|
||||||
app.post('/people/:id/clone/key', passport.authenticate(...bsc), (req, res) =>
|
app.post('/people/:id/clone/key', passport.authenticate(...bsc), (req, res) =>
|
||||||
Person.clone(req, res, tools)
|
People.clone(req, res, tools)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Read person
|
// Read person
|
||||||
app.get('/people/:id/jwt', passport.authenticate(...jwt), (req, res) =>
|
app.get('/people/:id/jwt', passport.authenticate(...jwt), (req, res) =>
|
||||||
Person.read(req, res, tools)
|
People.read(req, res, tools)
|
||||||
)
|
)
|
||||||
app.get('/people/:id/key', passport.authenticate(...bsc), (req, res) =>
|
app.get('/people/:id/key', passport.authenticate(...bsc), (req, res) =>
|
||||||
Person.read(req, res, tools)
|
People.read(req, res, tools)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Update person
|
// Update person
|
||||||
app.put('/people/:id/jwt', passport.authenticate(...jwt), (req, res) =>
|
app.put('/people/:id/jwt', passport.authenticate(...jwt), (req, res) =>
|
||||||
Person.update(req, res, tools)
|
People.update(req, res, tools)
|
||||||
)
|
)
|
||||||
app.put('/people/:id/key', passport.authenticate(...bsc), (req, res) =>
|
app.put('/people/:id/key', passport.authenticate(...bsc), (req, res) =>
|
||||||
Person.update(req, res, tools)
|
People.update(req, res, tools)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Delete person
|
// Delete person
|
||||||
app.delete('/people/:id/jwt', passport.authenticate(...jwt), (req, res) =>
|
app.delete('/people/:id/jwt', passport.authenticate(...jwt), (req, res) =>
|
||||||
Person.delete(req, res, tools)
|
People.delete(req, res, tools)
|
||||||
)
|
)
|
||||||
app.delete('/people/:id/key', passport.authenticate(...bsc), (req, res) =>
|
app.delete('/people/:id/key', passport.authenticate(...bsc), (req, res) =>
|
||||||
Person.delete(req, res, tools)
|
People.delete(req, res, tools)
|
||||||
)
|
)
|
||||||
}
|
}
|
|
@ -1,30 +1,38 @@
|
||||||
import { UserController } from '../controllers/user.mjs'
|
import { UsersController } from '../controllers/users.mjs'
|
||||||
|
|
||||||
const User = new UserController()
|
const Users = new UsersController()
|
||||||
const jwt = ['jwt', { session: false }]
|
const jwt = ['jwt', { session: false }]
|
||||||
const bsc = ['basic', { session: false }]
|
const bsc = ['basic', { session: false }]
|
||||||
|
|
||||||
export function userRoutes(tools) {
|
export function usersRoutes(tools) {
|
||||||
const { app, passport } = tools
|
const { app, passport } = tools
|
||||||
|
|
||||||
// Sign up
|
// Sign up
|
||||||
app.post('/signup', (req, res) => User.signup(req, res, tools))
|
app.post('/signup', (req, res) => Users.signup(req, res, tools))
|
||||||
|
|
||||||
// Confirm account
|
// Confirm account
|
||||||
app.post('/confirm/signup/:id', (req, res) => User.confirm(req, res, tools))
|
app.post('/confirm/signup/:id', (req, res) => Users.confirm(req, res, tools))
|
||||||
|
|
||||||
// Login
|
// Login
|
||||||
app.post('/login', (req, res) => User.login(req, res, tools))
|
app.post('/login', (req, res) => Users.login(req, res, tools))
|
||||||
|
|
||||||
// Read current jwt
|
// Read current jwt
|
||||||
|
|
||||||
app.get('/whoami/jwt', passport.authenticate(...jwt), (req, res) => User.whoami(req, res, tools))
|
app.get('/whoami/jwt', passport.authenticate(...jwt), (req, res) => Users.whoami(req, res, tools))
|
||||||
app.get('/account/jwt', passport.authenticate(...jwt), (req, res) => User.whoami(req, res, tools))
|
app.get('/account/jwt', passport.authenticate(...jwt), (req, res) =>
|
||||||
app.get('/account/key', passport.authenticate(...bsc), (req, res) => User.whoami(req, res, tools))
|
Users.whoami(req, res, tools)
|
||||||
|
)
|
||||||
|
app.get('/account/key', passport.authenticate(...bsc), (req, res) =>
|
||||||
|
Users.whoami(req, res, tools)
|
||||||
|
)
|
||||||
|
|
||||||
// Update account
|
// Update account
|
||||||
app.put('/account/jwt', passport.authenticate(...jwt), (req, res) => User.update(req, res, tools))
|
app.put('/account/jwt', passport.authenticate(...jwt), (req, res) =>
|
||||||
app.put('/account/key', passport.authenticate(...bsc), (req, res) => User.update(req, res, tools))
|
Users.update(req, res, tools)
|
||||||
|
)
|
||||||
|
app.put('/account/key', passport.authenticate(...bsc), (req, res) =>
|
||||||
|
Users.update(req, res, tools)
|
||||||
|
)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
@ -33,6 +33,7 @@ async function getAvatar(type, id) {
|
||||||
*/
|
*/
|
||||||
export const setUserAvatar = async (id, data) => setAvatar('user', id, data)
|
export const setUserAvatar = async (id, data) => setAvatar('user', id, data)
|
||||||
export const setPersonAvatar = async (id, data) => setAvatar('person', id, data)
|
export const setPersonAvatar = async (id, data) => setAvatar('person', id, data)
|
||||||
|
export const setPatternAvatar = async (id, data) => setAvatar('pattern', id, data)
|
||||||
export async function setAvatar(type, id, data) {
|
export async function setAvatar(type, id, data) {
|
||||||
// Step 1: Upload the image as asset
|
// Step 1: Upload the image as asset
|
||||||
const [contentType, binary] = b64ToBinaryWithType(data)
|
const [contentType, binary] = b64ToBinaryWithType(data)
|
||||||
|
|
|
@ -3,7 +3,7 @@ export const apikeyTests = async (chai, config, expect, store) => {
|
||||||
step(`${store.icon('key', 'jwt')} Create API Key (jwt)`, (done) => {
|
step(`${store.icon('key', 'jwt')} Create API Key (jwt)`, (done) => {
|
||||||
chai
|
chai
|
||||||
.request(config.api)
|
.request(config.api)
|
||||||
.post('/apikey/jwt')
|
.post('/apikeys/jwt')
|
||||||
.set('Authorization', 'Bearer ' + store.account.token)
|
.set('Authorization', 'Bearer ' + store.account.token)
|
||||||
.send({
|
.send({
|
||||||
name: 'Test API key',
|
name: 'Test API key',
|
||||||
|
@ -27,7 +27,7 @@ export const apikeyTests = async (chai, config, expect, store) => {
|
||||||
step(`${store.icon('key', 'key')} Create API Key (key)`, (done) => {
|
step(`${store.icon('key', 'key')} Create API Key (key)`, (done) => {
|
||||||
chai
|
chai
|
||||||
.request(config.api)
|
.request(config.api)
|
||||||
.post('/apikey/key')
|
.post('/apikeys/key')
|
||||||
.auth(store.apikey1.key, store.apikey1.secret)
|
.auth(store.apikey1.key, store.apikey1.secret)
|
||||||
.send({
|
.send({
|
||||||
name: 'Test API key with key',
|
name: 'Test API key with key',
|
||||||
|
@ -67,7 +67,7 @@ export const apikeyTests = async (chai, config, expect, store) => {
|
||||||
step(`${store.icon('key', 'key')} Read API key (key)`, (done) => {
|
step(`${store.icon('key', 'key')} Read API key (key)`, (done) => {
|
||||||
chai
|
chai
|
||||||
.request(config.api)
|
.request(config.api)
|
||||||
.get(`/apikey/${store.apikey1.key}/key`)
|
.get(`/apikeys/${store.apikey1.key}/key`)
|
||||||
.auth(store.apikey2.key, store.apikey2.secret)
|
.auth(store.apikey2.key, store.apikey2.secret)
|
||||||
.end((err, res) => {
|
.end((err, res) => {
|
||||||
expect(res.status).to.equal(200)
|
expect(res.status).to.equal(200)
|
||||||
|
@ -83,7 +83,7 @@ export const apikeyTests = async (chai, config, expect, store) => {
|
||||||
step(`${store.icon('key', 'jwt')} Read API key (jwt)`, (done) => {
|
step(`${store.icon('key', 'jwt')} Read API key (jwt)`, (done) => {
|
||||||
chai
|
chai
|
||||||
.request(config.api)
|
.request(config.api)
|
||||||
.get(`/apikey/${store.apikey2.key}/jwt`)
|
.get(`/apikeys/${store.apikey2.key}/jwt`)
|
||||||
.set('Authorization', 'Bearer ' + store.account.token)
|
.set('Authorization', 'Bearer ' + store.account.token)
|
||||||
.end((err, res) => {
|
.end((err, res) => {
|
||||||
expect(res.status).to.equal(200)
|
expect(res.status).to.equal(200)
|
||||||
|
@ -99,7 +99,7 @@ export const apikeyTests = async (chai, config, expect, store) => {
|
||||||
step(`${store.icon('key', 'key')} Remove API key (key)`, (done) => {
|
step(`${store.icon('key', 'key')} Remove API key (key)`, (done) => {
|
||||||
chai
|
chai
|
||||||
.request(config.api)
|
.request(config.api)
|
||||||
.delete(`/apikey/${store.apikey2.key}/key`)
|
.delete(`/apikeys/${store.apikey2.key}/key`)
|
||||||
.auth(store.apikey2.key, store.apikey2.secret)
|
.auth(store.apikey2.key, store.apikey2.secret)
|
||||||
.end((err, res) => {
|
.end((err, res) => {
|
||||||
expect(res.status).to.equal(204)
|
expect(res.status).to.equal(204)
|
||||||
|
@ -110,7 +110,7 @@ export const apikeyTests = async (chai, config, expect, store) => {
|
||||||
step(`${store.icon('key', 'jwt')} Remove API key (jwt)`, (done) => {
|
step(`${store.icon('key', 'jwt')} Remove API key (jwt)`, (done) => {
|
||||||
chai
|
chai
|
||||||
.request(config.api)
|
.request(config.api)
|
||||||
.delete(`/apikey/${store.apikey1.key}/jwt`)
|
.delete(`/apikeys/${store.apikey1.key}/jwt`)
|
||||||
.set('Authorization', 'Bearer ' + store.account.token)
|
.set('Authorization', 'Bearer ' + store.account.token)
|
||||||
.end((err, res) => {
|
.end((err, res) => {
|
||||||
expect(res.status).to.equal(204)
|
expect(res.status).to.equal(204)
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { userTests } from './user.mjs'
|
||||||
import { accountTests } from './account.mjs'
|
import { accountTests } from './account.mjs'
|
||||||
import { apikeyTests } from './apikey.mjs'
|
import { apikeyTests } from './apikey.mjs'
|
||||||
import { personTests } from './person.mjs'
|
import { personTests } from './person.mjs'
|
||||||
|
import { patternTests } from './pattern.mjs'
|
||||||
import { setup } from './shared.mjs'
|
import { setup } from './shared.mjs'
|
||||||
|
|
||||||
const runTests = async (...params) => {
|
const runTests = async (...params) => {
|
||||||
|
@ -9,6 +10,7 @@ const runTests = async (...params) => {
|
||||||
await apikeyTests(...params)
|
await apikeyTests(...params)
|
||||||
await accountTests(...params)
|
await accountTests(...params)
|
||||||
await personTests(...params)
|
await personTests(...params)
|
||||||
|
await patternTests(...params)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load initial data required for tests
|
// Load initial data required for tests
|
||||||
|
|
326
sites/backend/tests/pattern.mjs
Normal file
326
sites/backend/tests/pattern.mjs
Normal file
|
@ -0,0 +1,326 @@
|
||||||
|
import { cat } from './cat.mjs'
|
||||||
|
|
||||||
|
export const patternTests = async (chai, config, expect, store) => {
|
||||||
|
store.account.patterns = {}
|
||||||
|
for (const auth of ['jwt', 'key']) {
|
||||||
|
describe(`${store.icon('pattern', auth)} Pattern tests (${auth})`, () => {
|
||||||
|
it(`${store.icon('pattern', auth)} Should create a new pattern (${auth})`, (done) => {
|
||||||
|
chai
|
||||||
|
.request(config.api)
|
||||||
|
.post(`/patterns/${auth}`)
|
||||||
|
.set(
|
||||||
|
'Authorization',
|
||||||
|
auth === 'jwt'
|
||||||
|
? 'Bearer ' + store.account.token
|
||||||
|
: 'Basic ' +
|
||||||
|
new Buffer(`${store.account.apikey.key}:${store.account.apikey.secret}`).toString(
|
||||||
|
'base64'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.send({
|
||||||
|
design: 'aaron',
|
||||||
|
settings: {},
|
||||||
|
person: store.account.people.her.id,
|
||||||
|
})
|
||||||
|
.end((err, res) => {
|
||||||
|
expect(err === null).to.equal(true)
|
||||||
|
expect(res.status).to.equal(201)
|
||||||
|
expect(res.body.result).to.equal(`success`)
|
||||||
|
expect(typeof res.body.pattern?.id).to.equal('number')
|
||||||
|
expect(res.body.pattern.userId).to.equal(store.account.id)
|
||||||
|
expect(res.body.pattern.personId).to.equal(store.account.people.her.id)
|
||||||
|
expect(res.body.pattern.design).to.equal('aaron')
|
||||||
|
expect(res.body.pattern.public).to.equal(false)
|
||||||
|
store.account.patterns[auth] = res.body.pattern
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
}).timeout(5000)
|
||||||
|
|
||||||
|
for (const field of ['name', 'notes']) {
|
||||||
|
it(`${store.icon('pattern', auth)} Should update the ${field} field (${auth})`, (done) => {
|
||||||
|
const data = {}
|
||||||
|
const val = store.account.patterns[auth][field] + '_updated'
|
||||||
|
data[field] = val
|
||||||
|
chai
|
||||||
|
.request(config.api)
|
||||||
|
.put(`/patterns/${store.account.patterns[auth].id}/${auth}`)
|
||||||
|
.set(
|
||||||
|
'Authorization',
|
||||||
|
auth === 'jwt'
|
||||||
|
? 'Bearer ' + store.account.token
|
||||||
|
: 'Basic ' +
|
||||||
|
new Buffer(
|
||||||
|
`${store.account.apikey.key}:${store.account.apikey.secret}`
|
||||||
|
).toString('base64')
|
||||||
|
)
|
||||||
|
.send(data)
|
||||||
|
.end((err, res) => {
|
||||||
|
expect(err === null).to.equal(true)
|
||||||
|
expect(res.status).to.equal(200)
|
||||||
|
expect(res.body.result).to.equal(`success`)
|
||||||
|
expect(res.body.pattern[field]).to.equal('--_updated')
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
it(`${store.icon('person', auth)} Should update the public field (${auth})`, (done) => {
|
||||||
|
chai
|
||||||
|
.request(config.api)
|
||||||
|
.put(`/patterns/${store.account.patterns[auth].id}/${auth}`)
|
||||||
|
.set(
|
||||||
|
'Authorization',
|
||||||
|
auth === 'jwt'
|
||||||
|
? 'Bearer ' + store.account.token
|
||||||
|
: 'Basic ' +
|
||||||
|
new Buffer(`${store.account.apikey.key}:${store.account.apikey.secret}`).toString(
|
||||||
|
'base64'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.send({ public: true })
|
||||||
|
.end((err, res) => {
|
||||||
|
expect(err === null).to.equal(true)
|
||||||
|
expect(res.status).to.equal(200)
|
||||||
|
expect(res.body.result).to.equal(`success`)
|
||||||
|
expect(res.body.pattern.public).to.equal(true)
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it(`${store.icon('person', auth)} Should not update the design field (${auth})`, (done) => {
|
||||||
|
chai
|
||||||
|
.request(config.api)
|
||||||
|
.put(`/patterns/${store.account.patterns[auth].id}/${auth}`)
|
||||||
|
.set(
|
||||||
|
'Authorization',
|
||||||
|
auth === 'jwt'
|
||||||
|
? 'Bearer ' + store.account.token
|
||||||
|
: 'Basic ' +
|
||||||
|
new Buffer(`${store.account.apikey.key}:${store.account.apikey.secret}`).toString(
|
||||||
|
'base64'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.send({ design: 'updated' })
|
||||||
|
.end((err, res) => {
|
||||||
|
expect(err === null).to.equal(true)
|
||||||
|
expect(res.status).to.equal(200)
|
||||||
|
expect(res.body.result).to.equal(`success`)
|
||||||
|
expect(res.body.pattern.design).to.equal('aaron')
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it(`${store.icon('person', auth)} Should not update the person field (${auth})`, (done) => {
|
||||||
|
chai
|
||||||
|
.request(config.api)
|
||||||
|
.put(`/patterns/${store.account.patterns[auth].id}/${auth}`)
|
||||||
|
.set(
|
||||||
|
'Authorization',
|
||||||
|
auth === 'jwt'
|
||||||
|
? 'Bearer ' + store.account.token
|
||||||
|
: 'Basic ' +
|
||||||
|
new Buffer(`${store.account.apikey.key}:${store.account.apikey.secret}`).toString(
|
||||||
|
'base64'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.send({ person: 1 })
|
||||||
|
.end((err, res) => {
|
||||||
|
expect(err === null).to.equal(true)
|
||||||
|
expect(res.status).to.equal(200)
|
||||||
|
expect(res.body.result).to.equal(`success`)
|
||||||
|
expect(res.body.pattern.personId).to.equal(store.account.people.her.id)
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const field of ['data', 'settings']) {
|
||||||
|
it(`${store.icon('person', auth)} Should update the ${field} field (${auth})`, (done) => {
|
||||||
|
const data = {}
|
||||||
|
data[field] = { test: { value: 'hello' } }
|
||||||
|
chai
|
||||||
|
.request(config.api)
|
||||||
|
.put(`/patterns/${store.account.patterns[auth].id}/${auth}`)
|
||||||
|
.set(
|
||||||
|
'Authorization',
|
||||||
|
auth === 'jwt'
|
||||||
|
? 'Bearer ' + store.account.token
|
||||||
|
: 'Basic ' +
|
||||||
|
new Buffer(
|
||||||
|
`${store.account.apikey.key}:${store.account.apikey.secret}`
|
||||||
|
).toString('base64')
|
||||||
|
)
|
||||||
|
.send(data)
|
||||||
|
.end((err, res) => {
|
||||||
|
expect(err === null).to.equal(true)
|
||||||
|
expect(res.status).to.equal(200)
|
||||||
|
expect(res.body.result).to.equal(`success`)
|
||||||
|
expect(res.body.pattern[field].test.value).to.equal('hello')
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
it(`${store.icon('pattern', auth)} Should read a pattern (${auth})`, (done) => {
|
||||||
|
chai
|
||||||
|
.request(config.api)
|
||||||
|
.get(`/patterns/${store.account.patterns[auth].id}/${auth}`)
|
||||||
|
.set(
|
||||||
|
'Authorization',
|
||||||
|
auth === 'jwt'
|
||||||
|
? 'Bearer ' + store.account.token
|
||||||
|
: 'Basic ' +
|
||||||
|
new Buffer(`${store.account.apikey.key}:${store.account.apikey.secret}`).toString(
|
||||||
|
'base64'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.end((err, res) => {
|
||||||
|
expect(err === null).to.equal(true)
|
||||||
|
expect(res.status).to.equal(200)
|
||||||
|
expect(res.body.result).to.equal(`success`)
|
||||||
|
expect(res.body.pattern.data.test.value).to.equal('hello')
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it(`${store.icon(
|
||||||
|
'person',
|
||||||
|
auth
|
||||||
|
)} Should not allow reading another user's pattern (${auth})`, (done) => {
|
||||||
|
chai
|
||||||
|
.request(config.api)
|
||||||
|
.get(`/patterns/${store.account.patterns[auth].id}/${auth}`)
|
||||||
|
.set(
|
||||||
|
'Authorization',
|
||||||
|
auth === 'jwt'
|
||||||
|
? 'Bearer ' + store.altaccount.token
|
||||||
|
: 'Basic ' +
|
||||||
|
new Buffer(
|
||||||
|
`${store.altaccount.apikey.key}:${store.altaccount.apikey.secret}`
|
||||||
|
).toString('base64')
|
||||||
|
)
|
||||||
|
.end((err, res) => {
|
||||||
|
expect(err === null).to.equal(true)
|
||||||
|
expect(res.status).to.equal(403)
|
||||||
|
expect(res.body.result).to.equal(`error`)
|
||||||
|
expect(res.body.error).to.equal(`insufficientAccessLevel`)
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it(`${store.icon(
|
||||||
|
'person',
|
||||||
|
auth
|
||||||
|
)} Should not allow updating another user's pattern (${auth})`, (done) => {
|
||||||
|
chai
|
||||||
|
.request(config.api)
|
||||||
|
.put(`/patterns/${store.account.patterns[auth].id}/${auth}`)
|
||||||
|
.set(
|
||||||
|
'Authorization',
|
||||||
|
auth === 'jwt'
|
||||||
|
? 'Bearer ' + store.altaccount.token
|
||||||
|
: 'Basic ' +
|
||||||
|
new Buffer(
|
||||||
|
`${store.altaccount.apikey.key}:${store.altaccount.apikey.secret}`
|
||||||
|
).toString('base64')
|
||||||
|
)
|
||||||
|
.send({
|
||||||
|
name: 'I have been taken over',
|
||||||
|
})
|
||||||
|
.end((err, res) => {
|
||||||
|
expect(err === null).to.equal(true)
|
||||||
|
expect(res.status).to.equal(403)
|
||||||
|
expect(res.body.result).to.equal(`error`)
|
||||||
|
expect(res.body.error).to.equal(`insufficientAccessLevel`)
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it(`${store.icon(
|
||||||
|
'person',
|
||||||
|
auth
|
||||||
|
)} Should not allow removing another user's pattern (${auth})`, (done) => {
|
||||||
|
chai
|
||||||
|
.request(config.api)
|
||||||
|
.delete(`/patterns/${store.account.patterns[auth].id}/${auth}`)
|
||||||
|
.set(
|
||||||
|
'Authorization',
|
||||||
|
auth === 'jwt'
|
||||||
|
? 'Bearer ' + store.altaccount.token
|
||||||
|
: 'Basic ' +
|
||||||
|
new Buffer(
|
||||||
|
`${store.altaccount.apikey.key}:${store.altaccount.apikey.secret}`
|
||||||
|
).toString('base64')
|
||||||
|
)
|
||||||
|
.end((err, res) => {
|
||||||
|
expect(err === null).to.equal(true)
|
||||||
|
expect(res.status).to.equal(403)
|
||||||
|
expect(res.body.result).to.equal(`error`)
|
||||||
|
expect(res.body.error).to.equal(`insufficientAccessLevel`)
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
/*
|
||||||
|
it(`${store.icon('person', auth)} Should clone a person (${auth})`, (done) => {
|
||||||
|
chai
|
||||||
|
.request(config.api)
|
||||||
|
.post(`/people/${store.person[auth].id}/clone/${auth}`)
|
||||||
|
.set(
|
||||||
|
'Authorization',
|
||||||
|
auth === 'jwt'
|
||||||
|
? 'Bearer ' + store.account.token
|
||||||
|
: 'Basic ' +
|
||||||
|
new Buffer(`${store.account.apikey.key}:${store.account.apikey.secret}`).toString(
|
||||||
|
'base64'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.end((err, res) => {
|
||||||
|
expect(err === null).to.equal(true)
|
||||||
|
expect(res.status).to.equal(200)
|
||||||
|
expect(res.body.result).to.equal(`success`)
|
||||||
|
expect(typeof res.body.error).to.equal(`undefined`)
|
||||||
|
expect(typeof res.body.person.id).to.equal(`number`)
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it(`${store.icon(
|
||||||
|
'person',
|
||||||
|
auth
|
||||||
|
)} Should (not) clone a public person across accounts (${auth})`, (done) => {
|
||||||
|
chai
|
||||||
|
.request(config.api)
|
||||||
|
.post(`/people/${store.person[auth].id}/clone/${auth}`)
|
||||||
|
.set(
|
||||||
|
'Authorization',
|
||||||
|
auth === 'jwt'
|
||||||
|
? 'Bearer ' + store.altaccount.token
|
||||||
|
: 'Basic ' +
|
||||||
|
new Buffer(
|
||||||
|
`${store.altaccount.apikey.key}:${store.altaccount.apikey.secret}`
|
||||||
|
).toString('base64')
|
||||||
|
)
|
||||||
|
.end((err, res) => {
|
||||||
|
if (store.person[auth].public) {
|
||||||
|
expect(err === null).to.equal(true)
|
||||||
|
expect(res.status).to.equal(200)
|
||||||
|
expect(res.body.result).to.equal(`success`)
|
||||||
|
expect(typeof res.body.error).to.equal(`undefined`)
|
||||||
|
expect(typeof res.body.person.id).to.equal(`number`)
|
||||||
|
} else {
|
||||||
|
expect(err === null).to.equal(true)
|
||||||
|
expect(res.status).to.equal(403)
|
||||||
|
expect(res.body.result).to.equal(`error`)
|
||||||
|
expect(res.body.error).to.equal(`insufficientAccessLevel`)
|
||||||
|
}
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO:
|
||||||
|
// - Clone person
|
||||||
|
// - Clone person accross accounts of they are public
|
||||||
|
*/
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,12 +4,17 @@ import chai from 'chai'
|
||||||
import http from 'chai-http'
|
import http from 'chai-http'
|
||||||
import { verifyConfig } from '../src/config.mjs'
|
import { verifyConfig } from '../src/config.mjs'
|
||||||
import { randomString } from '../src/utils/crypto.mjs'
|
import { randomString } from '../src/utils/crypto.mjs'
|
||||||
|
import {
|
||||||
|
cisFemaleAdult34 as her,
|
||||||
|
cisMaleAdult42 as him,
|
||||||
|
} from '../../../packages/models/src/index.mjs'
|
||||||
|
|
||||||
dotenv.config()
|
dotenv.config()
|
||||||
|
|
||||||
const config = verifyConfig(true)
|
const config = verifyConfig(true)
|
||||||
const expect = chai.expect
|
const expect = chai.expect
|
||||||
chai.use(http)
|
chai.use(http)
|
||||||
|
const people = { her, him }
|
||||||
|
|
||||||
export const setup = async () => {
|
export const setup = async () => {
|
||||||
// Initial store contents
|
// Initial store contents
|
||||||
|
@ -21,17 +26,20 @@ export const setup = async () => {
|
||||||
email: `test_${randomString()}@${config.tests.domain}`,
|
email: `test_${randomString()}@${config.tests.domain}`,
|
||||||
language: 'en',
|
language: 'en',
|
||||||
password: randomString(),
|
password: randomString(),
|
||||||
|
people: {},
|
||||||
},
|
},
|
||||||
altaccount: {
|
altaccount: {
|
||||||
email: `test_${randomString()}@${config.tests.domain}`,
|
email: `test_${randomString()}@${config.tests.domain}`,
|
||||||
language: 'en',
|
language: 'en',
|
||||||
password: randomString(),
|
password: randomString(),
|
||||||
|
people: {},
|
||||||
},
|
},
|
||||||
icons: {
|
icons: {
|
||||||
user: '🧑 ',
|
user: '🧑 ',
|
||||||
jwt: '🎫 ',
|
jwt: '🎫 ',
|
||||||
key: '🎟️ ',
|
key: '🎟️ ',
|
||||||
person: '🧕 ',
|
person: '🧕 ',
|
||||||
|
pattern: '👕 ',
|
||||||
},
|
},
|
||||||
randomString,
|
randomString,
|
||||||
}
|
}
|
||||||
|
@ -63,12 +71,12 @@ export const setup = async () => {
|
||||||
}
|
}
|
||||||
store[acc].token = result.data.token
|
store[acc].token = result.data.token
|
||||||
store[acc].username = result.data.account.username
|
store[acc].username = result.data.account.username
|
||||||
store[acc].userid = result.data.account.id
|
store[acc].id = result.data.account.id
|
||||||
|
|
||||||
// Create API key
|
// Create API key
|
||||||
try {
|
try {
|
||||||
result = await axios.post(
|
result = await axios.post(
|
||||||
`${store.config.api}/apikey/jwt`,
|
`${store.config.api}/apikeys/jwt`,
|
||||||
{
|
{
|
||||||
name: 'Test API key',
|
name: 'Test API key',
|
||||||
level: 4,
|
level: 4,
|
||||||
|
@ -85,6 +93,29 @@ export const setup = async () => {
|
||||||
process.exit()
|
process.exit()
|
||||||
}
|
}
|
||||||
store[acc].apikey = result.data.apikey
|
store[acc].apikey = result.data.apikey
|
||||||
|
|
||||||
|
// Create people key
|
||||||
|
for (const name in people) {
|
||||||
|
try {
|
||||||
|
result = await axios.post(
|
||||||
|
`${store.config.api}/people/jwt`,
|
||||||
|
{
|
||||||
|
name: `This is ${name} name`,
|
||||||
|
name: `These are ${name} notes`,
|
||||||
|
measies: people[name],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
authorization: `Bearer ${store[acc].token}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} catch (err) {
|
||||||
|
console.log('Failed at API key creation request', err)
|
||||||
|
process.exit()
|
||||||
|
}
|
||||||
|
store[acc].people[name] = result.data.person
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { chai, config, expect, store }
|
return { chai, config, expect, store }
|
||||||
|
|
|
@ -184,12 +184,12 @@ export const userTests = async (chai, config, expect, store) => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
step(`${store.icon('user')} Should login with userid and password`, (done) => {
|
step(`${store.icon('user')} Should login with id and password`, (done) => {
|
||||||
chai
|
chai
|
||||||
.request(config.api)
|
.request(config.api)
|
||||||
.post('/login')
|
.post('/login')
|
||||||
.send({
|
.send({
|
||||||
username: store.account.userid,
|
username: store.account.id,
|
||||||
password: store.account.password,
|
password: store.account.password,
|
||||||
})
|
})
|
||||||
.end((err, res) => {
|
.end((err, res) => {
|
||||||
|
|
|
@ -11,14 +11,13 @@ import { designs, plugins } from '../../../config/software/index.mjs'
|
||||||
* srcPkgs: Array of folders in the monorepo/packages that should be aliased
|
* srcPkgs: Array of folders in the monorepo/packages that should be aliased
|
||||||
* so they are loaded from source, rather than from a compiled bundle
|
* so they are loaded from source, rather than from a compiled bundle
|
||||||
*/
|
*/
|
||||||
const config = (site, remarkPlugins=[]) => ({
|
const config = (site, remarkPlugins = []) => ({
|
||||||
experimental: {
|
experimental: {
|
||||||
externalDir: true,
|
externalDir: true,
|
||||||
},
|
},
|
||||||
pageExtensions: [ 'js', 'md', 'mjs' ],
|
pageExtensions: ['js', 'md', 'mjs'],
|
||||||
webpack: (config, options) => {
|
webpack: (config, options) => {
|
||||||
|
// Fixes npm packages that depend on node modules
|
||||||
// Fixes npm packages that depend on node modules
|
|
||||||
if (!options.isServer) {
|
if (!options.isServer) {
|
||||||
config.resolve.fallback.fs = false
|
config.resolve.fallback.fs = false
|
||||||
config.resolve.fallback.path = false
|
config.resolve.fallback.path = false
|
||||||
|
@ -34,35 +33,30 @@ const config = (site, remarkPlugins=[]) => ({
|
||||||
loader: '@mdx-js/loader',
|
loader: '@mdx-js/loader',
|
||||||
//providerImportSource: '@mdx-js/react',
|
//providerImportSource: '@mdx-js/react',
|
||||||
options: {
|
options: {
|
||||||
remarkPlugins: [
|
remarkPlugins: [remarkGfm, ...remarkPlugins],
|
||||||
remarkGfm,
|
},
|
||||||
...remarkPlugins,
|
},
|
||||||
]
|
],
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// YAML support
|
// YAML support
|
||||||
config.module.rules.push({
|
config.module.rules.push({
|
||||||
test: /\.ya?ml$/,
|
test: /\.ya?ml$/,
|
||||||
use: 'yaml-loader'
|
use: 'yaml-loader',
|
||||||
})
|
})
|
||||||
|
|
||||||
// Fix for nextjs bug #17806
|
// Fix for nextjs bug #17806
|
||||||
config.module.rules.push({
|
config.module.rules.push({
|
||||||
test: /index.mjs$/,
|
test: /index.mjs$/,
|
||||||
type: "javascript/auto",
|
type: 'javascript/auto',
|
||||||
resolve: {
|
resolve: {
|
||||||
fullySpecified: false
|
fullySpecified: false,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// Suppress warnings about importing version from package.json
|
// Suppress warnings about importing version from package.json
|
||||||
// We'll deal with it in v3 of FreeSewing
|
// We'll deal with it in v3 of FreeSewing
|
||||||
config.ignoreWarnings = [
|
config.ignoreWarnings = [/only default export is available soon/]
|
||||||
/only default export is available soon/
|
|
||||||
]
|
|
||||||
|
|
||||||
// Aliases
|
// Aliases
|
||||||
config.resolve.alias.shared = path.resolve('../shared/')
|
config.resolve.alias.shared = path.resolve('../shared/')
|
||||||
|
@ -75,19 +69,25 @@ const config = (site, remarkPlugins=[]) => ({
|
||||||
|
|
||||||
// Load designs from source, rather than compiled package
|
// Load designs from source, rather than compiled package
|
||||||
for (const design in designs) {
|
for (const design in designs) {
|
||||||
config.resolve.alias[`@freesewing/${design}$`] = path.resolve(`../../designs/${design}/src/index.mjs`)
|
config.resolve.alias[`@freesewing/${design}$`] = path.resolve(
|
||||||
|
`../../designs/${design}/src/index.mjs`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
// Load plugins from source, rather than compiled package
|
// Load plugins from source, rather than compiled package
|
||||||
for (const plugin in plugins) {
|
for (const plugin in plugins) {
|
||||||
config.resolve.alias[`@freesewing/${plugin}$`] = path.resolve(`../../plugins/${plugin}/src/index.mjs`)
|
config.resolve.alias[`@freesewing/${plugin}$`] = path.resolve(
|
||||||
|
`../../plugins/${plugin}/src/index.mjs`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
// Load these from source, rather than compiled package
|
// Load these from source, rather than compiled package
|
||||||
for (const pkg of ['core', 'config-helpers', 'i18n', 'models']) {
|
for (const pkg of ['core', 'i18n', 'models', 'snapseries']) {
|
||||||
config.resolve.alias[`@freesewing/${pkg}$`] = path.resolve(`../../packages/${pkg}/src/index.mjs`)
|
config.resolve.alias[`@freesewing/${pkg}$`] = path.resolve(
|
||||||
|
`../../packages/${pkg}/src/index.mjs`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return config
|
return config
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
export default config
|
export default config
|
||||||
|
|
|
@ -4,6 +4,7 @@ import chai from 'chai'
|
||||||
import { timingPlugin } from '@freesewing/plugin-timing'
|
import { timingPlugin } from '@freesewing/plugin-timing'
|
||||||
|
|
||||||
const expect = chai.expect
|
const expect = chai.expect
|
||||||
|
const ciTimeout = 10000
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* This runs unit tests for pattern drafting
|
* This runs unit tests for pattern drafting
|
||||||
|
@ -39,7 +40,7 @@ export const testPatternDrafting = (Pattern, log = false) => {
|
||||||
*/
|
*/
|
||||||
if (family !== 'utilities') {
|
if (family !== 'utilities') {
|
||||||
describe('Draft for humans:', function () {
|
describe('Draft for humans:', function () {
|
||||||
this.timeout(5000)
|
this.timeout(ciTimeout)
|
||||||
for (const type of ['cisFemale', 'cisMale']) {
|
for (const type of ['cisFemale', 'cisMale']) {
|
||||||
describe(type, () => {
|
describe(type, () => {
|
||||||
for (const size in adult[type]) {
|
for (const size in adult[type]) {
|
||||||
|
@ -62,7 +63,7 @@ export const testPatternDrafting = (Pattern, log = false) => {
|
||||||
const fams = { doll, giant }
|
const fams = { doll, giant }
|
||||||
for (const family of ['doll', 'giant']) {
|
for (const family of ['doll', 'giant']) {
|
||||||
describe(`Draft for ${family}:`, function () {
|
describe(`Draft for ${family}:`, function () {
|
||||||
this.timeout(5000)
|
this.timeout(ciTimeout)
|
||||||
for (const type of ['cisFemale', 'cisMale']) {
|
for (const type of ['cisFemale', 'cisMale']) {
|
||||||
describe(type, () => {
|
describe(type, () => {
|
||||||
for (const size in fams[family][type]) {
|
for (const size in fams[family][type]) {
|
||||||
|
|
|
@ -6,6 +6,7 @@ class TerseReporter {
|
||||||
constructor(runner) {
|
constructor(runner) {
|
||||||
runner.on(EVENT_TEST_FAIL, (test, err) => {
|
runner.on(EVENT_TEST_FAIL, (test, err) => {
|
||||||
console.log(` FAIL: ${test.fullTitle()}`)
|
console.log(` FAIL: ${test.fullTitle()}`)
|
||||||
|
console.log(err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue