mirror of
https://github.com/penpot/penpot.git
synced 2026-04-26 11:48:28 +00:00
Compare commits
No commits in common. "develop" and "2.16.0-RC1" have entirely different histories.
develop
...
2.16.0-RC1
15
.github/workflows/build-staging-render.yml
vendored
Normal file
15
.github/workflows/build-staging-render.yml
vendored
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
name: _STAGING RENDER
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
schedule:
|
||||||
|
- cron: '36 5-20 * * 1-5'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-bundle:
|
||||||
|
uses: ./.github/workflows/build-bundle.yml
|
||||||
|
secrets: inherit
|
||||||
|
with:
|
||||||
|
gh_ref: "staging-render"
|
||||||
|
build_wasm: "yes"
|
||||||
|
build_storybook: "yes"
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@ -50,7 +50,6 @@
|
|||||||
/frontend/.storybook/preview-body.html
|
/frontend/.storybook/preview-body.html
|
||||||
/frontend/.storybook/preview-head.html
|
/frontend/.storybook/preview-head.html
|
||||||
/frontend/playwright-report/
|
/frontend/playwright-report/
|
||||||
/frontend/playwright/ui/visual-specs/
|
|
||||||
/frontend/text-editor/src/wasm/
|
/frontend/text-editor/src/wasm/
|
||||||
/frontend/dist/
|
/frontend/dist/
|
||||||
/frontend/npm-debug.log
|
/frontend/npm-debug.log
|
||||||
@ -82,4 +81,3 @@
|
|||||||
/**/node_modules
|
/**/node_modules
|
||||||
/**/.yarn/*
|
/**/.yarn/*
|
||||||
/.pnpm-store
|
/.pnpm-store
|
||||||
/.vscode
|
|
||||||
|
|||||||
@ -1,27 +0,0 @@
|
|||||||
---
|
|
||||||
name: commiter
|
|
||||||
description: Git commit assistant following CONTRIBUTING.md commit rules
|
|
||||||
mode: primary
|
|
||||||
---
|
|
||||||
|
|
||||||
Role: You are responsible for creating git commits for Penpot and must follow
|
|
||||||
the repository commit-format rules exactly.
|
|
||||||
|
|
||||||
Requirements:
|
|
||||||
|
|
||||||
* Read `CONTRIBUTING.md` before creating any commit and follow the
|
|
||||||
commit guidelines strictly.
|
|
||||||
* Use commit messages in the form `:emoji: <imperative subject>`.
|
|
||||||
* Keep the subject capitalized, concise, 70 characters or fewer, and
|
|
||||||
without a trailing period.
|
|
||||||
* Keep the description (commit body) with maximum line length of 80
|
|
||||||
characters. Use manual line breaks to wrap text before it exceeds
|
|
||||||
this limit.
|
|
||||||
* Separate the subject from the body with a blank line.
|
|
||||||
* Write a clear and concise body when needed.
|
|
||||||
* Use `git commit -s` so the commit includes the required
|
|
||||||
`Signed-off-by` line.
|
|
||||||
* Do not guess or hallucinate git author information (Name or
|
|
||||||
Email). Never include the `--author` flag in git commands unless
|
|
||||||
specifically instructed by the user for a unique case; assume the
|
|
||||||
local environment is already configured.
|
|
||||||
30
AGENTS.md
30
AGENTS.md
@ -32,36 +32,6 @@ precision while maintaining a strong focus on maintainability and performance.
|
|||||||
5. When searching code, prefer `ripgrep` (`rg`) over `grep` — it respects
|
5. When searching code, prefer `ripgrep` (`rg`) over `grep` — it respects
|
||||||
`.gitignore` by default.
|
`.gitignore` by default.
|
||||||
|
|
||||||
## GitHub Operations
|
|
||||||
|
|
||||||
To obtain the list of repository members/collaborators:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
gh api repos/:owner/:repo/collaborators --paginate --jq '.[].login'
|
|
||||||
```
|
|
||||||
|
|
||||||
To obtain the list of open PRs authored by members:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
MEMBERS=$(gh api repos/:owner/:repo/collaborators --paginate --jq '.[].login' | tr '\n' '|' | sed 's/|$//')
|
|
||||||
gh pr list --state open --limit 200 --json author,title,number | jq -r --arg members "$MEMBERS" '
|
|
||||||
($members | split("|")) as $m |
|
|
||||||
.[] | select(.author.login as $a | $m | index($a)) |
|
|
||||||
"\(.number)\t\(.author.login)\t\(.title)"
|
|
||||||
'
|
|
||||||
```
|
|
||||||
|
|
||||||
To obtain the list of open PRs from external contributors (non-members):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
MEMBERS=$(gh api repos/:owner/:repo/collaborators --paginate --jq '.[].login' | tr '\n' '|' | sed 's/|$//')
|
|
||||||
gh pr list --state open --limit 200 --json author,title,number | jq -r --arg members "$MEMBERS" '
|
|
||||||
($members | split("|")) as $m |
|
|
||||||
.[] | select(.author.login as $a | $m | index($a) | not) |
|
|
||||||
"\(.number)\t\(.author.login)\t\(.title)"
|
|
||||||
'
|
|
||||||
```
|
|
||||||
|
|
||||||
## Architecture Overview
|
## Architecture Overview
|
||||||
|
|
||||||
Penpot is an open-source design tool composed of several modules:
|
Penpot is an open-source design tool composed of several modules:
|
||||||
|
|||||||
100
CHANGES.md
100
CHANGES.md
@ -1,91 +1,5 @@
|
|||||||
# CHANGELOG
|
# CHANGELOG
|
||||||
|
|
||||||
## 2.17.0 (Unreleased)
|
|
||||||
|
|
||||||
### :boom: Breaking changes & Deprecations
|
|
||||||
|
|
||||||
### :rocket: Epics and highlights
|
|
||||||
|
|
||||||
- Add MCP server integration [Taiga #13112](https://tree.taiga.io/project/penpot/us/13112)
|
|
||||||
|
|
||||||
### :sparkles: New features & Enhancements
|
|
||||||
|
|
||||||
- Show alpha percentage next to library color values to distinguish colors that differ only in opacity (by @rockchris099) [Github #6328](https://github.com/penpot/penpot/issues/6328)
|
|
||||||
- Add "Clear artboard guides" option to right-click context menu for frames (by @eureka0928) [Github #6987](https://github.com/penpot/penpot/issues/6987)
|
|
||||||
- Add loader feedback while importing and exporting files [Github #9020](https://github.com/penpot/penpot/issues/9020)
|
|
||||||
- Allow duplicating color and typography styles (by @MkDev11) [Github #2912](https://github.com/penpot/penpot/issues/2912)
|
|
||||||
- Add woff2 support on user uploaded fonts (by @Nivl) [Github #8248](https://github.com/penpot/penpot/pull/8248)
|
|
||||||
- Import Tokens from linked library (by @dfelinto) [Github #8391](https://github.com/penpot/penpot/pull/8391)
|
|
||||||
- Option to download custom fonts (by @dfelinto) [Github #8320](https://github.com/penpot/penpot/issues/8320)
|
|
||||||
- Add copy as image to clipboard option to workspace context menu (by @dfelinto) [Github #8313](https://github.com/penpot/penpot/pull/8313)
|
|
||||||
- Add Tab/Shift+Tab navigation to rename layers sequentially (by @bittoby) [Github #8474](https://github.com/penpot/penpot/pull/8474)
|
|
||||||
- Copy and paste entire rows in existing table (by @bittoby) [Github #8498](https://github.com/penpot/penpot/pull/8498)
|
|
||||||
- Rename token group [Taiga #13137](https://tree.taiga.io/project/penpot/us/13137)
|
|
||||||
- Duplicate token group [Taiga #10653](https://tree.taiga.io/project/penpot/us/10653)
|
|
||||||
- Copy token name from contextual menu [Taiga #13568](https://tree.taiga.io/project/penpot/issue/13568)
|
|
||||||
- Add natural sorting on token names [Taiga #13713](https://tree.taiga.io/project/penpot/issue/13713)
|
|
||||||
- Add drag-to-change for numeric inputs in workspace sidebar [Github #2466](https://github.com/penpot/penpot/issues/2466)
|
|
||||||
- Add CSS linter [Taiga #13790](https://tree.taiga.io/project/penpot/us/13790)
|
|
||||||
- Save and restore selection state in undo/redo (by @eureka0928) [Github #6007](https://github.com/penpot/penpot/issues/6007)
|
|
||||||
- Fix warnings for unsupported token $type (by @Dexterity104) [Github #8790](https://github.com/penpot/penpot/issues/8790)
|
|
||||||
- Add per-group add button for typographies (by @eureka0928) [Github #5275](https://github.com/penpot/penpot/issues/5275)
|
|
||||||
- Add Find & Replace for text content and layer names (by @statxc) [Github #7108](https://github.com/penpot/penpot/issues/7108)
|
|
||||||
- Use page name for multi-export ZIP/PDF downloads (by @Dexterity104) [Github #8773](https://github.com/penpot/penpot/issues/8773)
|
|
||||||
- Make links in comments clickable (by @eureka0928) [Github #1602](https://github.com/penpot/penpot/issues/1602)
|
|
||||||
- Add visibility toggle for strokes (by @eureka0928) [Github #7438](https://github.com/penpot/penpot/issues/7438)
|
|
||||||
- Sort asset library subfolders alphabetically at every nesting level (by @eureka0928) [Github #2572](https://github.com/penpot/penpot/issues/2572)
|
|
||||||
- Add Paste to replace (Cmd+Shift+V) to replace the selected shape with clipboard contents (by @eureka0928) [Github #4240](https://github.com/penpot/penpot/issues/4240)
|
|
||||||
- Differentiate incoming and outgoing interaction link colors (by @claytonlin1110) [Github #7794](https://github.com/penpot/penpot/issues/7794)
|
|
||||||
- Add guide locking and fix locked elements not selectable in viewer (by @Dexterity104) [Github #8358](https://github.com/penpot/penpot/issues/8358)
|
|
||||||
- Apply styles to selection (by @AzazelN28) [Taiga #13647](https://tree.taiga.io/project/penpot/task/13647)
|
|
||||||
- Reorder prototyping overlay options to show Position before Relative to (by @rockchris099) [Github #2910](https://github.com/penpot/penpot/issues/2910)
|
|
||||||
- Add customizable colors for ruler guides (by @Dexterity104) [Github #5199](https://github.com/penpot/penpot/issues/5199)
|
|
||||||
- Persist asset search query and section filter when switching sidebar tabs (by @eureka0928) [Github #2913](https://github.com/penpot/penpot/issues/2913)
|
|
||||||
- Add delete and duplicate buttons to typography dialog (by @eureka0928) [Github #5270](https://github.com/penpot/penpot/issues/5270)
|
|
||||||
- Edit ruler guide position by double-clicking the guide pill (by @eureka0928) [Github #2311](https://github.com/penpot/penpot/issues/2311)
|
|
||||||
- Add a search bar to filter colors in the color palette toolbar (by @eureka0928) [Github #7653](https://github.com/penpot/penpot/issues/7653)
|
|
||||||
- Allow customising the OIDC login button label (by @wdeveloper16) [Github #7027](https://github.com/penpot/penpot/issues/7027)
|
|
||||||
- Add page separators in Workspace [Taiga #13611](https://tree.taiga.io/project/penpot/us/13611?milestone=262806)
|
|
||||||
- Add Shift+Numpad0/1/2 as aliases to Shift+0/1/2 for zoom shortcuts [Github #2457](https://github.com/penpot/penpot/issues/2457)
|
|
||||||
|
|
||||||
### :bug: Bugs fixed
|
|
||||||
|
|
||||||
- Fix `PENPOT_OIDC_USER_INFO_SOURCE` flag being silently ignored (`userinfo` / `token`) in the OIDC callback, causing "incomplete user info" failures during registration [Github #9108](https://github.com/penpot/penpot/issues/9108)
|
|
||||||
- Fix `get-view-only-bundle` crashing when a share-link viewer encounters a team member whose email lacks `@` (NullPointerException in `obfuscate-email`) or whose domain has no `.` (previously produced a dangling-dot `****@****.`); now the viewer-side obfuscation is nil-safe and omits the trailing dot when the domain has no TLD
|
|
||||||
- Remove `corepack` from the MCP local launcher so it runs on Node.js 25+, where corepack is no longer bundled [Github #8877](https://github.com/penpot/penpot/issues/8877)
|
|
||||||
- Fix Copy as SVG: emit a single valid SVG document when multiple shapes are selected, and publish `image/svg+xml` to the clipboard so the paste target works in Inkscape and other SVG-native tools [Github #838](https://github.com/penpot/penpot/issues/838)
|
|
||||||
- Reset profile submenu state when the account menu closes (by @eureka0928) [Github #8947](https://github.com/penpot/penpot/issues/8947)
|
|
||||||
- Add export panel to inspect styles tab [Taiga #13582](https://tree.taiga.io/project/penpot/issue/13582)
|
|
||||||
- Fix styles between grid layout inputs [Taiga #13526](https://tree.taiga.io/project/penpot/issue/13526)
|
|
||||||
- Fix id prop on switch component [Taiga #13534](https://tree.taiga.io/project/penpot/issue/13534)
|
|
||||||
- Update copy on penpot update message [Taiga #12924](https://tree.taiga.io/project/penpot/issue/12924)
|
|
||||||
- Fix scroll on library modal [Taiga #13639](https://tree.taiga.io/project/penpot/issue/13639)
|
|
||||||
- Fix dates to avoid show them in english when browser is in auto [Taiga #13786](https://tree.taiga.io/project/penpot/issue/13786)
|
|
||||||
- Fix focus radio button [Taiga #13841](https://tree.taiga.io/project/penpot/issue/13841)
|
|
||||||
- Token tree should be expanded by default [Taiga #13631](https://tree.taiga.io/project/penpot/issue/13631)
|
|
||||||
- Fix opacity incorrectly disabled for visible shapes [Taiga #13906](https://tree.taiga.io/project/penpot/issue/13906)
|
|
||||||
- Update onboarding image [Taiga #13864](https://tree.taiga.io/project/penpot/issue/13864)
|
|
||||||
- Fix plugin modal drag interactions over iframe and close-button behavior (by @marekhrabe) [Github #8871](https://github.com/penpot/penpot/pull/8871)
|
|
||||||
- Fix hot update on color-row on texts [Taiga #13923](https://tree.taiga.io/project/penpot/issue/13923)
|
|
||||||
- Fix selected color tokens [Taiga #13930](https://tree.taiga.io/project/penpot/issue/13930)
|
|
||||||
- Fix dashboard Recent/Deleted titles overlapped by scrolling content (by @rockchris099) [Github #8577](https://github.com/penpot/penpot/issues/8577)
|
|
||||||
- Display resolved values of inactive tokens [Taiga #13628](https://tree.taiga.io/project/penpot/issue/13628)
|
|
||||||
- Fix hyphens stripped from export filenames (by @jamesrayammons) [Github #8901](https://github.com/penpot/penpot/issues/8901)
|
|
||||||
- Fix app crash when selecting shapes with one hidden [Taiga #13959](https://tree.taiga.io/project/penpot/issue/13959)
|
|
||||||
- Fix opacity mixed value [Taiga #13960](https://tree.taiga.io/project/penpot/issue/13960)
|
|
||||||
- Fix gap input throwing an error [Github #8984](https://github.com/penpot/penpot/pull/8984)
|
|
||||||
- Fix non-functional clear icon in change email modal inputs (by @Dexterity104) [Github #8977](https://github.com/penpot/penpot/issues/8977)
|
|
||||||
- Disable save button after saving account profile settings (by @Dexterity104) [Github #8979](https://github.com/penpot/penpot/issues/8979)
|
|
||||||
- Fix copy to be more specific [Taiga #13990](https://tree.taiga.io/project/penpot/issue/13990)
|
|
||||||
- Allow deleting the profile avatar after uploading [Github #9067](https://github.com/penpot/penpot/issues/9067)
|
|
||||||
- Fix incorrect rendering when exporting text as SVG, PNG and JPG (by @edwin-rivera-dev) [Github #8516](https://github.com/penpot/penpot/issues/8516)
|
|
||||||
- Fix Settings and Notifications "Update Settings" button enabled state when form has no changes (by @moorsecopers99) [Github #9090](https://github.com/penpot/penpot/issues/9090)
|
|
||||||
- Fix "Help & Learning" submenu vertical alignment in account menu (by @juan-flores077) [Github #9137](https://github.com/penpot/penpot/issues/9137)
|
|
||||||
- Fix plugin `addInteraction` silently rejecting `open-overlay` actions with `manualPositionLocation` [Github #8409](https://github.com/penpot/penpot/issues/8409)
|
|
||||||
- Fix typography style creation with tokenized line-height (by @juan-flores077) [Github #8479](https://github.com/penpot/penpot/issues/8479)
|
|
||||||
- Fix colorpicker layout so the eyedropper button is visible again [Taiga #14057](https://tree.taiga.io/project/penpot/issue/14057)
|
|
||||||
|
|
||||||
|
|
||||||
## 2.16.0 (Unreleased)
|
## 2.16.0 (Unreleased)
|
||||||
|
|
||||||
### :boom: Breaking changes & Deprecations
|
### :boom: Breaking changes & Deprecations
|
||||||
@ -108,8 +22,6 @@
|
|||||||
- Fix id prop on switch component [Taiga #13534](https://tree.taiga.io/project/penpot/issue/13534)
|
- Fix id prop on switch component [Taiga #13534](https://tree.taiga.io/project/penpot/issue/13534)
|
||||||
- Fix dashboard navigation tabs overlap with projects content when scrolling [Taiga #13962](https://tree.taiga.io/project/penpot/issue/13962)
|
- Fix dashboard navigation tabs overlap with projects content when scrolling [Taiga #13962](https://tree.taiga.io/project/penpot/issue/13962)
|
||||||
- Fix text editor v1 focus [Taiga #13961](https://tree.taiga.io/project/penpot/issue/13961)
|
- Fix text editor v1 focus [Taiga #13961](https://tree.taiga.io/project/penpot/issue/13961)
|
||||||
- Fix color dropdown option update [Taiga #14035](https://tree.taiga.io/project/penpot/issue/14035)
|
|
||||||
- Fix themes modal height [Taiga #14046](https://tree.taiga.io/project/penpot/issue/14046)
|
|
||||||
|
|
||||||
|
|
||||||
## 2.15.0 (Unreleased)
|
## 2.15.0 (Unreleased)
|
||||||
@ -122,14 +34,6 @@
|
|||||||
### :bug: Bugs fixed
|
### :bug: Bugs fixed
|
||||||
|
|
||||||
- Fix incorrect handling of version restore operation [Github #9041](https://github.com/penpot/penpot/pull/9041)
|
- Fix incorrect handling of version restore operation [Github #9041](https://github.com/penpot/penpot/pull/9041)
|
||||||
|
|
||||||
|
|
||||||
## 2.14.4
|
|
||||||
|
|
||||||
### :bug: Bugs fixed
|
|
||||||
|
|
||||||
- Fix email validation [Taiga #14006](https://tree.taiga.io/project/penpot/issue/14006)
|
|
||||||
- Fix email blacklisting [Github #9122](https://github.com/penpot/penpot/pull/9122)
|
|
||||||
- Fix removeChild errors from unmount race conditions [Github #8927](https://github.com/penpot/penpot/pull/8927)
|
- Fix removeChild errors from unmount race conditions [Github #8927](https://github.com/penpot/penpot/pull/8927)
|
||||||
|
|
||||||
|
|
||||||
@ -161,7 +65,6 @@
|
|||||||
- Fix wrong `mapcat` call in `collect-main-shapes`
|
- Fix wrong `mapcat` call in `collect-main-shapes`
|
||||||
- Fix stale accumulator in `get-children-in-instance` recursion
|
- Fix stale accumulator in `get-children-in-instance` recursion
|
||||||
- Fix typo `:podition` in swap-shapes grid cell
|
- Fix typo `:podition` in swap-shapes grid cell
|
||||||
- Fix multiple selection on shapes with token applied to stroke color
|
|
||||||
|
|
||||||
|
|
||||||
## 2.14.2
|
## 2.14.2
|
||||||
@ -186,6 +89,7 @@
|
|||||||
- Guard delete undo against missing sibling order [Github #8858](https://github.com/penpot/penpot/pull/8858)
|
- Guard delete undo against missing sibling order [Github #8858](https://github.com/penpot/penpot/pull/8858)
|
||||||
- Fix ICounted error on numeric-input token dropdown keyboard nav [Github #8803](https://github.com/penpot/penpot/pull/8803)
|
- Fix ICounted error on numeric-input token dropdown keyboard nav [Github #8803](https://github.com/penpot/penpot/pull/8803)
|
||||||
|
|
||||||
|
|
||||||
## 2.14.1
|
## 2.14.1
|
||||||
|
|
||||||
### :sparkles: New features & Enhancements
|
### :sparkles: New features & Enhancements
|
||||||
@ -209,6 +113,7 @@
|
|||||||
- Ensure path content is always PathData when saving
|
- Ensure path content is always PathData when saving
|
||||||
- Fix error when get-parent-with-data encounters non-Element nodes
|
- Fix error when get-parent-with-data encounters non-Element nodes
|
||||||
|
|
||||||
|
|
||||||
## 2.14.0
|
## 2.14.0
|
||||||
|
|
||||||
### :boom: Breaking changes & Deprecations
|
### :boom: Breaking changes & Deprecations
|
||||||
@ -278,7 +183,6 @@
|
|||||||
## 2.13.0
|
## 2.13.0
|
||||||
|
|
||||||
### :heart: Community contributions (Thank you!)
|
### :heart: Community contributions (Thank you!)
|
||||||
|
|
||||||
- Add 'page' special shapeId to MCP export_shape tool for full-page snapshots [Github #8689](https://github.com/penpot/penpot/issues/8689)
|
- Add 'page' special shapeId to MCP export_shape tool for full-page snapshots [Github #8689](https://github.com/penpot/penpot/issues/8689)
|
||||||
|
|
||||||
- Fix mask issues with component swap (by @dfelinto) [Github #7675](https://github.com/penpot/penpot/issues/7675)
|
- Fix mask issues with component swap (by @dfelinto) [Github #7675](https://github.com/penpot/penpot/issues/7675)
|
||||||
|
|||||||
91
README.md
91
README.md
@ -9,39 +9,45 @@
|
|||||||
</picture>
|
</picture>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://www.mozilla.org/en-US/MPL/2.0" rel="nofollow"><img alt="License: MPL-2.0" src="https://img.shields.io/badge/MPL-2.0-blue.svg" style="max-width:100%;"></a>
|
<a href="https://www.mozilla.org/en-US/MPL/2.0" rel="nofollow"><img alt="License: MPL-2.0" src="https://img.shields.io/badge/MPL-2.0-blue.svg" style="max-width:100%;"></a>
|
||||||
<a href="https://community.penpot.app" rel="nofollow"><img alt="Penpot Community" src="https://img.shields.io/discourse/posts?server=https%3A%2F%2Fcommunity.penpot.app" style="max-width:100%;"></a>
|
<a href="https://community.penpot.app" rel="nofollow"><img alt="Penpot Community" src="https://img.shields.io/discourse/posts?server=https%3A%2F%2Fcommunity.penpot.app" style="max-width:100%;"></a>
|
||||||
<a href="https://tree.taiga.io/project/penpot/" title="Managed with Taiga.io" rel="nofollow"><img alt="Managed with Taiga.io" src="https://img.shields.io/badge/managed%20with-TAIGA.io-709f14.svg" style="max-width:100%;"></a>
|
<a href="https://tree.taiga.io/project/penpot/" title="Managed with Taiga.io" rel="nofollow"><img alt="Managed with Taiga.io" src="https://img.shields.io/badge/managed%20with-TAIGA.io-709f14.svg" style="max-width:100%;"></a>
|
||||||
<a href="https://gitpod.io/#https://github.com/penpot/penpot" rel="nofollow"><img alt="Gitpod ready-to-code" src="https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod" style="max-width:100%;"></a>
|
<a href="https://gitpod.io/#https://github.com/penpot/penpot" rel="nofollow"><img alt="Gitpod ready-to-code" src="https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod" style="max-width:100%;"></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://penpot.app/"><b>Website</b></a> •
|
<a href="https://penpot.app/"><b>Website</b></a> •
|
||||||
<a href="https://help.penpot.app/user-guide/"><b>User Guide</b></a> •
|
<a href="https://help.penpot.app/user-guide/"><b>User Guide</b></a> •
|
||||||
<a href="https://penpot.app/learning-center"><b>Learning Center</b></a> •
|
<a href="https://penpot.app/learning-center"><b>Learning Center</b></a> •
|
||||||
<a href="https://community.penpot.app/"><b>Community</b></a>
|
<a href="https://community.penpot.app/"><b>Community</b></a>
|
||||||
</p>
|
</p>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://www.youtube.com/@Penpot"><b>Youtube</b></a> •
|
<a href="https://www.youtube.com/@Penpot"><b>Youtube</b></a> •
|
||||||
<a href="https://peertube.kaleidos.net/a/penpot_app/video-channels"><b>Peertube</b></a> •
|
<a href="https://peertube.kaleidos.net/a/penpot_app/video-channels"><b>Peertube</b></a> •
|
||||||
<a href="https://www.linkedin.com/company/penpot/"><b>Linkedin</b></a> •
|
<a href="https://www.linkedin.com/company/penpot/"><b>Linkedin</b></a> •
|
||||||
<a href="https://instagram.com/penpot.app"><b>Instagram</b></a> •
|
<a href="https://instagram.com/penpot.app"><b>Instagram</b></a> •
|
||||||
<a href="https://fosstodon.org/@penpot/"><b>Mastodon</b></a> •
|
<a href="https://fosstodon.org/@penpot/"><b>Mastodon</b></a> •
|
||||||
<a href="https://bsky.app/profile/penpot.app"><b>Bluesky</b></a> •
|
<a href="https://bsky.app/profile/penpot.app"><b>Bluesky</b></a> •
|
||||||
<a href="https://twitter.com/penpotapp"><b>X</b></a>
|
<a href="https://twitter.com/penpotapp"><b>X</b></a>
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
[Penpot video](https://github.com/user-attachments/assets/7c67fd7c-04d3-4c9b-88ec-b6f5e23f8332)
|
<br />
|
||||||
|
|
||||||
|
[Penpot video](https://github.com/user-attachments/assets/7c67fd7c-04d3-4c9b-88ec-b6f5e23f8332
|
||||||
|
)
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
Penpot is the first **open-source** design tool for design and code collaboration. Designers can create stunning designs, interactive prototypes, design systems at scale, while developers enjoy ready-to-use code and make their workflow easy and fast. And all of this with no handoff drama.
|
Penpot is the first **open-source** design tool for design and code collaboration. Designers can create stunning designs, interactive prototypes, design systems at scale, while developers enjoy ready-to-use code and make their workflow easy and fast. And all of this with no handoff drama.
|
||||||
|
|
||||||
Available on browser or self-hosted, Penpot works with open standards like SVG, CSS, HTML and JSON, and it’s free!
|
Available on browser or self-hosted, Penpot works with open standards like SVG, CSS, HTML and JSON, and it’s free!
|
||||||
|
|
||||||
The latest updates take Penpot even further. It’s the first design tool to integrate native [design tokens](https://penpot.dev/collaboration/design-tokens)—a single source of truth to improve efficiency and collaboration between product design and development.
|
The latest updates take Penpot even further. It’s the first design tool to integrate native [design tokens](https://penpot.dev/collaboration/design-tokens)—a single source of truth to improve efficiency and collaboration between product design and development.
|
||||||
|
|
||||||
With the [huge 2.0 release](https://penpot.app/dev-diaries), Penpot took the platform to a whole new level. This update introduces the ground-breaking [CSS Grid Layout feature](https://penpot.app/penpot-2.0), a complete UI redesign, a new Components system, and much more.
|
With the [huge 2.0 release](https://penpot.app/dev-diaries), Penpot took the platform to a whole new level. This update introduces the ground-breaking [CSS Grid Layout feature](https://penpot.app/penpot-2.0), a complete UI redesign, a new Components system, and much more.
|
||||||
|
For organizations that need extra service for its teams, [get in touch](https://cal.com/team/penpot/talk-to-us)
|
||||||
|
|
||||||
For organizations that need extra service for its teams, [get in touch](https://cal.com/team/penpot/talk-to-us).
|
🎇 Design, code, and Open Source meet at [Penpot Fest](https://penpot.app/penpotfest)! Be part of the 2025 edition in Madrid, Spain, on October 9-10.
|
||||||
|
|
||||||
## Table of contents ##
|
## Table of contents ##
|
||||||
|
|
||||||
@ -57,42 +63,43 @@ For organizations that need extra service for its teams, [get in touch](https://
|
|||||||
Penpot expresses designs as code. Designers can do their best work and see it will be beautifully implemented by developers in a two-way collaboration.
|
Penpot expresses designs as code. Designers can do their best work and see it will be beautifully implemented by developers in a two-way collaboration.
|
||||||
|
|
||||||
### Plugin system ###
|
### Plugin system ###
|
||||||
|
|
||||||
[Penpot plugins](https://penpot.app/penpothub/plugins) let you expand the platform's capabilities, give you the flexibility to integrate it with other apps, and design custom solutions.
|
[Penpot plugins](https://penpot.app/penpothub/plugins) let you expand the platform's capabilities, give you the flexibility to integrate it with other apps, and design custom solutions.
|
||||||
|
|
||||||
### Designed for developers ###
|
### Designed for developers ###
|
||||||
|
|
||||||
Penpot was built to serve both designers and developers and create a fluid design-code process. You have the choice to enjoy real-time collaboration or play "solo".
|
Penpot was built to serve both designers and developers and create a fluid design-code process. You have the choice to enjoy real-time collaboration or play "solo".
|
||||||
|
|
||||||
### Inspect mode ###
|
### Inspect mode ###
|
||||||
|
|
||||||
Work with ready-to-use code and make your workflow easy and fast. The inspect tab gives instant access to SVG, CSS and HTML code.
|
Work with ready-to-use code and make your workflow easy and fast. The inspect tab gives instant access to SVG, CSS and HTML code.
|
||||||
|
|
||||||
### Self host your own instance ###
|
### Self host your own instance ###
|
||||||
|
|
||||||
Provide your team or organization with a completely owned collaborative design tool. Use Penpot's cloud service or deploy your own Penpot server.
|
Provide your team or organization with a completely owned collaborative design tool. Use Penpot's cloud service or deploy your own Penpot server.
|
||||||
|
|
||||||
### Integrations ###
|
### Integrations ###
|
||||||
|
|
||||||
Penpot offers integration into the development toolchain, thanks to its support for webhooks and an API accessible through access tokens.
|
Penpot offers integration into the development toolchain, thanks to its support for webhooks and an API accessible through access tokens.
|
||||||
|
|
||||||
### Building Design Systems: design tokens, components and variants ###
|
### Building Design Systems: design tokens, components and variants ###
|
||||||
|
|
||||||
Penpot brings design systems to code-minded teams: a single source of truth with native Design Tokens, Components, and Variants for scalable, reusable, and consistent UI across projects and platforms.
|
Penpot brings design systems to code-minded teams: a single source of truth with native Design Tokens, Components, and Variants for scalable, reusable, and consistent UI across projects and platforms.
|
||||||
|
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="https://github.com/user-attachments/assets/cce75ad6-f783-473f-8803-da9eb8255fef">
|
<img src="https://github.com/user-attachments/assets/cce75ad6-f783-473f-8803-da9eb8255fef">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
## Getting started ##
|
## Getting started ##
|
||||||
|
|
||||||
Penpot is the only design & prototype platform that is deployment agnostic. You can use it in our [SAAS](https://design.penpot.app) or deploy it anywhere.
|
Penpot is the only design & prototype platform that is deployment agnostic. You can use it in our [SAAS](https://design.penpot.app) or deploy it anywhere.
|
||||||
|
|
||||||
Learn how to install it with Docker, Kubernetes, Elestio or other options on [our website](https://penpot.app/self-host).
|
Learn how to install it with Docker, Kubernetes, Elestio or other options on [our website](https://penpot.app/self-host).
|
||||||
|
<br />
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="https://github.com/user-attachments/assets/93578500-2dbd-4045-a180-e640ea5b3bd5" style="width: 65%;">
|
<img src="https://site-assets.plasmic.app/2168cf524dd543caeff32384eb9ea0a1.svg" alt="Open Source" style="width: 65%;">
|
||||||
</p>
|
</p>
|
||||||
|
<br />
|
||||||
|
|
||||||
## Community ##
|
## Community ##
|
||||||
|
|
||||||
@ -101,7 +108,6 @@ We love the Open Source software community. Contributing is our passion and if i
|
|||||||
If you need help or have any questions; if you’d like to share your experience using Penpot or get inspired; if you’d rather meet our community of developers and designers, [join our Community](https://community.penpot.app/)!
|
If you need help or have any questions; if you’d like to share your experience using Penpot or get inspired; if you’d rather meet our community of developers and designers, [join our Community](https://community.penpot.app/)!
|
||||||
|
|
||||||
You will find the following categories:
|
You will find the following categories:
|
||||||
|
|
||||||
- [Ask the Community](https://community.penpot.app/c/ask-for-help-using-penpot/6)
|
- [Ask the Community](https://community.penpot.app/c/ask-for-help-using-penpot/6)
|
||||||
- [Troubleshooting](https://community.penpot.app/c/technical/8)
|
- [Troubleshooting](https://community.penpot.app/c/technical/8)
|
||||||
- [Help us Improve Penpot](https://community.penpot.app/c/help-us-improve-penpot/7)
|
- [Help us Improve Penpot](https://community.penpot.app/c/help-us-improve-penpot/7)
|
||||||
@ -111,36 +117,45 @@ You will find the following categories:
|
|||||||
- [Penpot in your language](https://community.penpot.app/c/penpot-in-your-language/12)
|
- [Penpot in your language](https://community.penpot.app/c/penpot-in-your-language/12)
|
||||||
- [Design and Code Essentials](https://community.penpot.app/c/design-and-code-essentials/22)
|
- [Design and Code Essentials](https://community.penpot.app/c/design-and-code-essentials/22)
|
||||||
|
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="https://github.com/user-attachments/assets/7b7d0f6b-a579-4822-a9ae-d3d5a9fc9d19" alt="Community" style="width: 65%;">
|
<img src="https://github.com/penpot/penpot/assets/5446186/6ac62220-a16c-46c9-ab21-d24ae357ed03" alt="Community" style="width: 65%;">
|
||||||
</p>
|
</p>
|
||||||
|
<br />
|
||||||
|
|
||||||
### Code of Conduct ###
|
### Code of Conduct ###
|
||||||
|
|
||||||
Anyone who contributes to Penpot, whether through code, in the community, or at an event, must adhere to the
|
Anyone who contributes to Penpot, whether through code, in the community, or at an event, must adhere to the
|
||||||
[code of conduct](https://help.penpot.app/contributing-guide/coc/) and foster a positive and safe environment.
|
[code of conduct](https://help.penpot.app/contributing-guide/coc/) and foster a positive and safe environment.
|
||||||
|
|
||||||
|
|
||||||
## Contributing ##
|
## Contributing ##
|
||||||
|
|
||||||
Any contribution will make a difference to improve Penpot. How can you get involved?
|
Any contribution will make a difference to improve Penpot. How can you get involved?
|
||||||
|
|
||||||
Choose your way:
|
Choose your way:
|
||||||
|
|
||||||
- Create and [share Libraries & Templates](https://penpot.app/libraries-templates.html) that will be helpful for the community.
|
- Create and [share Libraries & Templates](https://penpot.app/libraries-templates.html) that will be helpful for the community
|
||||||
- Invite your [team to join](https://design.penpot.app/#/auth/register).
|
- Invite your [team to join](https://design.penpot.app/#/auth/register)
|
||||||
- Give this repo a star and follow us on Social Media: [Mastodon](https://fosstodon.org/@penpot/), [Youtube](https://www.youtube.com/c/Penpot), [Instagram](https://instagram.com/penpot.app), [Linkedin](https://www.linkedin.com/company/penpotdesign), [Peertube](https://peertube.kaleidos.net/a/penpot_app), [X](https://twitter.com/penpotapp) and [BlueSky](https://bsky.app/profile/penpot.app).
|
- Give this repo a star and follow us on Social Media: [Mastodon](https://fosstodon.org/@penpot/), [Youtube](https://www.youtube.com/c/Penpot), [Instagram](https://instagram.com/penpot.app), [Linkedin](https://www.linkedin.com/company/penpotdesign), [Peertube](https://peertube.kaleidos.net/a/penpot_app), [X](https://twitter.com/penpotapp) and [BlueSky](https://bsky.app/profile/penpot.app)
|
||||||
- Participate in the [Community](https://community.penpot.app/) space by asking and answering questions; reacting to others’ articles; opening your own conversations and following along on decisions affecting the project.
|
- Participate in the [Community](https://community.penpot.app/) space by asking and answering questions; reacting to others’ articles; opening your own conversations and following along on decisions affecting the project.
|
||||||
- Report bugs with our easy [guide for bugs hunting](https://help.penpot.app/contributing-guide/reporting-bugs/) or [GitHub issues](https://github.com/penpot/penpot/issues).
|
- Report bugs with our easy [guide for bugs hunting](https://help.penpot.app/contributing-guide/reporting-bugs/) or [GitHub issues](https://github.com/penpot/penpot/issues)
|
||||||
- Become a [translator](https://help.penpot.app/contributing-guide/translations).
|
- Become a [translator](https://help.penpot.app/contributing-guide/translations)
|
||||||
- Give feedback: [Email us](mailto:support@penpot.app).
|
- Give feedback: [Email us](mailto:support@penpot.app)
|
||||||
- **Contribute to Penpot's code:** [Watch this video](https://www.youtube.com/watch?v=TpN0osiY-8k) by Alejandro Alonso, CIO and developer at Penpot, where he gives us a hands-on demo of how to use Penpot’s repository and make changes in both front and back end.
|
- **Contribute to Penpot's code:** [Watch this video](https://www.youtube.com/watch?v=TpN0osiY-8k) by Alejandro Alonso, CIO and developer at Penpot, where he gives us a hands-on demo of how to use Penpot’s repository and make changes in both front and back end
|
||||||
|
|
||||||
To find (almost) everything you need to know on how to contribute to Penpot, refer to the [contributing guide](https://help.penpot.app/contributing-guide/).
|
To find (almost) everything you need to know on how to contribute to Penpot, refer to the [contributing guide](https://help.penpot.app/contributing-guide/).
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="https://github.com/penpot/penpot/assets/5446186/fea18923-dc06-49be-86ad-c3496a7956e6" alt="Libraries and templates" style="width: 65%;">
|
<img src="https://github.com/penpot/penpot/assets/5446186/fea18923-dc06-49be-86ad-c3496a7956e6" alt="Libraries and templates" style="width: 65%;">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
## Resources ##
|
## Resources ##
|
||||||
|
|
||||||
You can ask and answer questions, have open-ended conversations, and follow along on decisions affecting the project.
|
You can ask and answer questions, have open-ended conversations, and follow along on decisions affecting the project.
|
||||||
@ -155,14 +170,14 @@ You can ask and answer questions, have open-ended conversations, and follow alon
|
|||||||
|
|
||||||
📚 [Dev Diaries](https://penpot.app/dev-diaries.html)
|
📚 [Dev Diaries](https://penpot.app/dev-diaries.html)
|
||||||
|
|
||||||
|
|
||||||
## License ##
|
## License ##
|
||||||
|
|
||||||
```text
|
```
|
||||||
This Source Code Form is subject to the terms of the Mozilla Public
|
This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
Copyright (c) KALEIDOS INC
|
Copyright (c) KALEIDOS INC
|
||||||
```
|
```
|
||||||
|
|
||||||
Penpot is a Kaleidos’ [open source project](https://kaleidos.net/)
|
Penpot is a Kaleidos’ [open source project](https://kaleidos.net/)
|
||||||
|
|||||||
@ -1,264 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
|
|
||||||
xmlns:o="urn:schemas-microsoft-com:office:office">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<title>
|
|
||||||
</title>
|
|
||||||
<!--[if !mso]><!-- -->
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
||||||
<!--<![endif]-->
|
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<style type="text/css">
|
|
||||||
#outlook a {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
-webkit-text-size-adjust: 100%;
|
|
||||||
-ms-text-size-adjust: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
table,
|
|
||||||
td {
|
|
||||||
border-collapse: collapse;
|
|
||||||
mso-table-lspace: 0pt;
|
|
||||||
mso-table-rspace: 0pt;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
border: 0;
|
|
||||||
height: auto;
|
|
||||||
line-height: 100%;
|
|
||||||
outline: none;
|
|
||||||
text-decoration: none;
|
|
||||||
-ms-interpolation-mode: bicubic;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
display: block;
|
|
||||||
margin: 13px 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<!--[if mso]>
|
|
||||||
<xml>
|
|
||||||
<o:OfficeDocumentSettings>
|
|
||||||
<o:AllowPNG/>
|
|
||||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
|
||||||
</o:OfficeDocumentSettings>
|
|
||||||
</xml>
|
|
||||||
<![endif]-->
|
|
||||||
<!--[if lte mso 11]>
|
|
||||||
<style type="text/css">
|
|
||||||
.mj-outlook-group-fix { width:100% !important; }
|
|
||||||
</style>
|
|
||||||
<![endif]-->
|
|
||||||
<!--[if !mso]><!-->
|
|
||||||
<link href="https://fonts.googleapis.com/css?family=Source%20Sans%20Pro" rel="stylesheet" type="text/css">
|
|
||||||
<style type="text/css">
|
|
||||||
@import url(https://fonts.googleapis.com/css?family=Source%20Sans%20Pro);
|
|
||||||
</style>
|
|
||||||
<!--<![endif]-->
|
|
||||||
<style type="text/css">
|
|
||||||
@media only screen and (min-width:480px) {
|
|
||||||
.mj-column-per-100 {
|
|
||||||
width: 100% !important;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mj-column-px-425 {
|
|
||||||
width: 425px !important;
|
|
||||||
max-width: 425px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<style type="text/css">
|
|
||||||
@media only screen and (max-width:480px) {
|
|
||||||
table.mj-full-width-mobile {
|
|
||||||
width: 100% !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
td.mj-full-width-mobile {
|
|
||||||
width: auto !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body style="background-color:#E5E5E5;">
|
|
||||||
<div style="background-color:#E5E5E5;">
|
|
||||||
<!--[if mso | IE]>
|
|
||||||
<table
|
|
||||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
|
||||||
>
|
|
||||||
<tr>
|
|
||||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
|
||||||
<![endif]-->
|
|
||||||
<div style="margin:0px auto;max-width:600px;">
|
|
||||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="direction:ltr;font-size:0px;padding:0;text-align:center;">
|
|
||||||
<!--[if mso | IE]>
|
|
||||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
|
|
||||||
<td
|
|
||||||
class="" style="vertical-align:top;width:600px;"
|
|
||||||
>
|
|
||||||
<![endif]-->
|
|
||||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
|
||||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
|
||||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
|
||||||
width="100%">
|
|
||||||
<tr>
|
|
||||||
<td align="left" style="font-size:0px;padding:16px;word-break:break-word;">
|
|
||||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
|
||||||
style="border-collapse:collapse;border-spacing:0px;">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="width:97px;">
|
|
||||||
<img height="32" src="{{ public-uri }}/images/email/uxbox-title.png"
|
|
||||||
style="border:0;display:block;outline:none;text-decoration:none;height:32px;width:100%;font-size:13px;"
|
|
||||||
width="97" />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<!--[if mso | IE]>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
</table>
|
|
||||||
<![endif]-->
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<!--[if mso | IE]>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<table
|
|
||||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
|
||||||
>
|
|
||||||
<tr>
|
|
||||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
|
||||||
<![endif]-->
|
|
||||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
|
||||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
|
||||||
style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
|
|
||||||
<!--[if mso | IE]>
|
|
||||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
|
|
||||||
<td
|
|
||||||
class="" style="vertical-align:top;width:600px;"
|
|
||||||
>
|
|
||||||
<![endif]-->
|
|
||||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
|
||||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
|
||||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
|
||||||
width="100%">
|
|
||||||
<tr>
|
|
||||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
|
||||||
<div
|
|
||||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
|
||||||
Hi{% if user-name %} {{ user-name|abbreviate:25 }}{% endif %},
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
|
||||||
<div
|
|
||||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
|
||||||
<b>{{invited-by|abbreviate:25}}</b> sent you an invitation to join the organization:
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
|
||||||
<div
|
|
||||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
|
||||||
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="20" height="20" style="display:inline-block;vertical-align:middle;">
|
|
||||||
<tr>
|
|
||||||
<td width="20" height="20" align="center" valign="middle"
|
|
||||||
background="{{org-logo}}"
|
|
||||||
style="width:20px;height:20px;text-align:center;font-weight:bold;font-size:9px;line-height:20px;color:#ffffff;background-size:cover;background-position:center;background-repeat:no-repeat;border-radius: 50%;color:black">
|
|
||||||
{{org-initials}}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
<span style="display:inline-block; vertical-align: middle;padding-left:5px;height:20px;line-height: 20px;">
|
|
||||||
“{{ organization-name|abbreviate:25 }}”
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center" vertical-align="middle"
|
|
||||||
style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
|
||||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
|
||||||
style="border-collapse:separate;line-height:100%;">
|
|
||||||
<tr>
|
|
||||||
<td align="center" bgcolor="#6911d4" role="presentation"
|
|
||||||
style="border:none;border-radius:8px;cursor:auto;mso-padding-alt:10px 25px;background:#6911d4;"
|
|
||||||
valign="middle">
|
|
||||||
<a href="{{ public-uri }}/#/auth/verify-token?token={{token}}"
|
|
||||||
style="display:inline-block;background:#6911d4;color:#FFFFFF;font-family:Source Sans Pro, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:8px;"
|
|
||||||
target="_blank"> ACCEPT INVITE </a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
|
||||||
<div
|
|
||||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
|
||||||
Enjoy!</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
|
||||||
<div
|
|
||||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
|
||||||
The Penpot team.</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<!--[if mso | IE]>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
</table>
|
|
||||||
<![endif]-->
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% include "app/email/includes/footer.html" %}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{{invited-by|abbreviate:25}} has invited you to join the organization “{{ organization-name|abbreviate:25 }}”
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
Hello!
|
|
||||||
|
|
||||||
{{invited-by|abbreviate:25}} has invited you to join the organization “{{ organization-name|abbreviate:25 }}”.
|
|
||||||
|
|
||||||
Accept invitation using this link:
|
|
||||||
|
|
||||||
{{ public-uri }}/#/auth/verify-token?token={{token}}
|
|
||||||
|
|
||||||
Enjoy!
|
|
||||||
The Penpot team.
|
|
||||||
@ -186,8 +186,7 @@
|
|||||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||||
<div
|
<div
|
||||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||||
{{invited-by|abbreviate:25}} has invited you to join the team “{{ team|abbreviate:25 }}”{% if organization %}
|
{{invited-by|abbreviate:25}} has invited you to join the team “{{ team|abbreviate:25 }}”.</div>
|
||||||
part of the organization “{{ organization|abbreviate:25 }}”{% endif %}.</div>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
Hello!
|
Hello!
|
||||||
|
|
||||||
{{invited-by|abbreviate:25}} has invited you to join the team "{{ team|abbreviate:25 }}"{% if organization %}, part of the organization "{{ organization|abbreviate:25 }}"{% endif %}.
|
{{invited-by|abbreviate:25}} has invited you to join the team “{{ team|abbreviate:25 }}”.
|
||||||
|
|
||||||
Accept invitation using this link:
|
Accept invitation using this link:
|
||||||
|
|
||||||
|
|||||||
@ -401,9 +401,8 @@
|
|||||||
|
|
||||||
(defn- parse-attr-path
|
(defn- parse-attr-path
|
||||||
[provider path]
|
[provider path]
|
||||||
(let [separator (if (str/includes? path "__") "__" ".")
|
(let [[fitem & items] (str/split path "__")]
|
||||||
[fitem & items] (str/split path separator)]
|
(into [(keyword (:type provider) fitem)] (map keyword) items)))
|
||||||
(into [(keyword (:type provider) (str/kebab fitem))] (map keyword) items)))
|
|
||||||
|
|
||||||
(defn- build-redirect-uri
|
(defn- build-redirect-uri
|
||||||
[]
|
[]
|
||||||
@ -489,9 +488,9 @@
|
|||||||
(let [attr-ph (parse-attr-path provider "nickname")]
|
(let [attr-ph (parse-attr-path provider "nickname")]
|
||||||
(get-in props attr-ph))))]
|
(get-in props attr-ph))))]
|
||||||
|
|
||||||
(let [info (assoc info :provider-id (str (:id provider)))
|
(let [info (assoc info :provider-id (str (:id provider)))
|
||||||
props (qualify-props provider info)
|
props (qualify-props provider info)
|
||||||
email (get-email props)]
|
email (get-email props)]
|
||||||
{:backend (:type provider)
|
{:backend (:type provider)
|
||||||
:fullname (or (get-name props) email)
|
:fullname (or (get-name props) email)
|
||||||
:email email
|
:email email
|
||||||
@ -548,29 +547,16 @@
|
|||||||
(def ^:private valid-info?
|
(def ^:private valid-info?
|
||||||
(sm/validator schema:info))
|
(sm/validator schema:info))
|
||||||
|
|
||||||
(defn- select-user-info-source
|
|
||||||
"Normalise the provider's configured user-info source into a keyword the
|
|
||||||
dispatch below can match. The raw value comes from config as a string
|
|
||||||
per the malli schema in `app.config` (`\"token\"`, `\"userinfo\"`, or
|
|
||||||
`\"auto\"`) and from hard-coded per-provider maps as strings as well;
|
|
||||||
any unrecognised or missing value falls back to `:auto` (prefer claims,
|
|
||||||
use userinfo as fallback)."
|
|
||||||
[source]
|
|
||||||
(case source
|
|
||||||
"token" :token
|
|
||||||
"userinfo" :userinfo
|
|
||||||
:auto))
|
|
||||||
|
|
||||||
(defn- get-info
|
(defn- get-info
|
||||||
[cfg provider state code]
|
[cfg provider state code]
|
||||||
(let [tdata (fetch-access-token cfg provider code)
|
(let [tdata (fetch-access-token cfg provider code)
|
||||||
claims (get-id-token-claims provider tdata)
|
claims (get-id-token-claims provider tdata)
|
||||||
|
|
||||||
info (case (select-user-info-source (get provider :user-info-source))
|
info (case (get provider :user-info-source)
|
||||||
:token (dissoc claims :exp :iss :iat :aud :sid)
|
:token (dissoc claims :exp :iss :iat :aud :sub :sid)
|
||||||
:userinfo (fetch-user-info cfg provider tdata)
|
:userinfo (fetch-user-info cfg provider tdata)
|
||||||
:auto (or (some-> claims (dissoc :exp :iss :iat :aud :sid))
|
(or (some-> claims (dissoc :exp :iss :iat :aud :sub :sid))
|
||||||
(fetch-user-info cfg provider tdata)))
|
(fetch-user-info cfg provider tdata)))
|
||||||
|
|
||||||
info (process-user-info provider tdata info)]
|
info (process-user-info provider tdata info)]
|
||||||
|
|
||||||
|
|||||||
@ -40,8 +40,8 @@
|
|||||||
[promesa.util :as pu]
|
[promesa.util :as pu]
|
||||||
[yetti.adapter :as yt])
|
[yetti.adapter :as yt])
|
||||||
(:import
|
(:import
|
||||||
com.github.luben.zstd.ZstdInputStream
|
|
||||||
com.github.luben.zstd.ZstdIOException
|
com.github.luben.zstd.ZstdIOException
|
||||||
|
com.github.luben.zstd.ZstdInputStream
|
||||||
com.github.luben.zstd.ZstdOutputStream
|
com.github.luben.zstd.ZstdOutputStream
|
||||||
java.io.DataInputStream
|
java.io.DataInputStream
|
||||||
java.io.DataOutputStream
|
java.io.DataOutputStream
|
||||||
|
|||||||
@ -36,11 +36,11 @@
|
|||||||
java.sql.Connection
|
java.sql.Connection
|
||||||
java.sql.PreparedStatement
|
java.sql.PreparedStatement
|
||||||
java.sql.Savepoint
|
java.sql.Savepoint
|
||||||
|
org.postgresql.PGConnection
|
||||||
org.postgresql.geometric.PGpoint
|
org.postgresql.geometric.PGpoint
|
||||||
org.postgresql.jdbc.PgArray
|
org.postgresql.jdbc.PgArray
|
||||||
org.postgresql.largeobject.LargeObject
|
org.postgresql.largeobject.LargeObject
|
||||||
org.postgresql.largeobject.LargeObjectManager
|
org.postgresql.largeobject.LargeObjectManager
|
||||||
org.postgresql.PGConnection
|
|
||||||
org.postgresql.util.PGInterval
|
org.postgresql.util.PGInterval
|
||||||
org.postgresql.util.PGobject))
|
org.postgresql.util.PGobject))
|
||||||
|
|
||||||
|
|||||||
@ -22,13 +22,13 @@
|
|||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[integrant.core :as ig])
|
[integrant.core :as ig])
|
||||||
(:import
|
(:import
|
||||||
|
jakarta.mail.Message$RecipientType
|
||||||
|
jakarta.mail.Session
|
||||||
|
jakarta.mail.Transport
|
||||||
jakarta.mail.internet.InternetAddress
|
jakarta.mail.internet.InternetAddress
|
||||||
jakarta.mail.internet.MimeBodyPart
|
jakarta.mail.internet.MimeBodyPart
|
||||||
jakarta.mail.internet.MimeMessage
|
jakarta.mail.internet.MimeMessage
|
||||||
jakarta.mail.internet.MimeMultipart
|
jakarta.mail.internet.MimeMultipart
|
||||||
jakarta.mail.Message$RecipientType
|
|
||||||
jakarta.mail.Session
|
|
||||||
jakarta.mail.Transport
|
|
||||||
java.util.Properties))
|
java.util.Properties))
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
@ -412,21 +412,6 @@
|
|||||||
:id ::invite-to-team
|
:id ::invite-to-team
|
||||||
:schema schema:invite-to-team))
|
:schema schema:invite-to-team))
|
||||||
|
|
||||||
(def ^:private schema:invite-to-org
|
|
||||||
[:map
|
|
||||||
[:invited-by ::sm/text]
|
|
||||||
[:organization-name ::sm/text]
|
|
||||||
[:org-initials ::sm/text]
|
|
||||||
[:org-logo ::sm/uri]
|
|
||||||
[:user-name [:maybe ::sm/text]]
|
|
||||||
[:token ::sm/text]])
|
|
||||||
|
|
||||||
(def invite-to-org
|
|
||||||
"Org member invitation email."
|
|
||||||
(template-factory
|
|
||||||
:id ::invite-to-org
|
|
||||||
:schema schema:invite-to-org))
|
|
||||||
|
|
||||||
(def ^:private schema:join-team
|
(def ^:private schema:join-team
|
||||||
[:map
|
[:map
|
||||||
[:invited-by ::sm/text]
|
[:invited-by ::sm/text]
|
||||||
|
|||||||
@ -36,18 +36,10 @@
|
|||||||
:cause cause)))))
|
:cause cause)))))
|
||||||
|
|
||||||
(defn contains?
|
(defn contains?
|
||||||
"Check if email is in the blacklist. Also matches subdomains: if
|
"Check if email is in the blacklist."
|
||||||
'somedomain.com' is blacklisted, 'xxx@foo.somedomain.com' will also
|
|
||||||
be rejected."
|
|
||||||
[{:keys [::email/blacklist]} email]
|
[{:keys [::email/blacklist]} email]
|
||||||
(let [[_ domain] (str/split email "@" 2)
|
(let [[_ domain] (str/split email "@" 2)]
|
||||||
parts (str/split (str/lower domain) #"\.")]
|
(c/contains? blacklist (str/lower domain))))
|
||||||
(loop [parts parts]
|
|
||||||
(if (empty? parts)
|
|
||||||
false
|
|
||||||
(if (c/contains? blacklist (str/join "." parts))
|
|
||||||
true
|
|
||||||
(recur (rest parts)))))))
|
|
||||||
|
|
||||||
(defn enabled?
|
(defn enabled?
|
||||||
"Check if the blacklist is enabled"
|
"Check if the blacklist is enabled"
|
||||||
|
|||||||
@ -112,9 +112,8 @@
|
|||||||
THEN (c.deleted_at IS NULL OR c.deleted_at >= ?::timestamptz)
|
THEN (c.deleted_at IS NULL OR c.deleted_at >= ?::timestamptz)
|
||||||
END"))
|
END"))
|
||||||
|
|
||||||
(defn get-snapshot-data
|
(defn- get-snapshot
|
||||||
"Get a fully decoded snapshot for read-only preview or restoration.
|
"Get snapshot with decoded data"
|
||||||
Returns the snapshot map with decoded :data field."
|
|
||||||
[cfg file-id snapshot-id]
|
[cfg file-id snapshot-id]
|
||||||
(let [now (ct/now)]
|
(let [now (ct/now)]
|
||||||
(->> (db/get-with-sql cfg [sql:get-snapshot file-id snapshot-id now]
|
(->> (db/get-with-sql cfg [sql:get-snapshot file-id snapshot-id now]
|
||||||
@ -327,7 +326,7 @@
|
|||||||
(sto/resolve cfg {::db/reuse-conn true})
|
(sto/resolve cfg {::db/reuse-conn true})
|
||||||
|
|
||||||
snapshot
|
snapshot
|
||||||
(get-snapshot-data cfg file-id snapshot-id)]
|
(get-snapshot cfg file-id snapshot-id)]
|
||||||
|
|
||||||
(when-not snapshot
|
(when-not snapshot
|
||||||
(ex/raise :type :not-found
|
(ex/raise :type :not-found
|
||||||
|
|||||||
@ -31,8 +31,8 @@
|
|||||||
(:import
|
(:import
|
||||||
clojure.lang.XMLHandler
|
clojure.lang.XMLHandler
|
||||||
java.io.InputStream
|
java.io.InputStream
|
||||||
javax.xml.parsers.SAXParserFactory
|
|
||||||
javax.xml.XMLConstants
|
javax.xml.XMLConstants
|
||||||
|
javax.xml.parsers.SAXParserFactory
|
||||||
org.apache.commons.io.IOUtils
|
org.apache.commons.io.IOUtils
|
||||||
org.im4java.core.ConvertCmd
|
org.im4java.core.ConvertCmd
|
||||||
org.im4java.core.IMOperation))
|
org.im4java.core.IMOperation))
|
||||||
|
|||||||
@ -15,16 +15,16 @@
|
|||||||
io.prometheus.client.CollectorRegistry
|
io.prometheus.client.CollectorRegistry
|
||||||
io.prometheus.client.Counter
|
io.prometheus.client.Counter
|
||||||
io.prometheus.client.Counter$Child
|
io.prometheus.client.Counter$Child
|
||||||
io.prometheus.client.exporter.common.TextFormat
|
|
||||||
io.prometheus.client.Gauge
|
io.prometheus.client.Gauge
|
||||||
io.prometheus.client.Gauge$Child
|
io.prometheus.client.Gauge$Child
|
||||||
io.prometheus.client.Histogram
|
io.prometheus.client.Histogram
|
||||||
io.prometheus.client.Histogram$Child
|
io.prometheus.client.Histogram$Child
|
||||||
io.prometheus.client.hotspot.DefaultExports
|
|
||||||
io.prometheus.client.SimpleCollector
|
io.prometheus.client.SimpleCollector
|
||||||
io.prometheus.client.Summary
|
io.prometheus.client.Summary
|
||||||
io.prometheus.client.Summary$Builder
|
io.prometheus.client.Summary$Builder
|
||||||
io.prometheus.client.Summary$Child
|
io.prometheus.client.Summary$Child
|
||||||
|
io.prometheus.client.exporter.common.TextFormat
|
||||||
|
io.prometheus.client.hotspot.DefaultExports
|
||||||
java.io.StringWriter))
|
java.io.StringWriter))
|
||||||
|
|
||||||
(set! *warn-on-reflection* true)
|
(set! *warn-on-reflection* true)
|
||||||
|
|||||||
@ -471,9 +471,6 @@
|
|||||||
{:name "0146-mod-access-token-table"
|
{:name "0146-mod-access-token-table"
|
||||||
:fn (mg/resource "app/migrations/sql/0146-mod-access-token-table.sql")}
|
:fn (mg/resource "app/migrations/sql/0146-mod-access-token-table.sql")}
|
||||||
|
|
||||||
{:name "0147-mod-team-invitation-table"
|
|
||||||
:fn (mg/resource "app/migrations/sql/0147-mod-team-invitation-table.sql")}
|
|
||||||
|
|
||||||
{:name "0147-add-upload-session-table"
|
{:name "0147-add-upload-session-table"
|
||||||
:fn (mg/resource "app/migrations/sql/0147-add-upload-session-table.sql")}])
|
:fn (mg/resource "app/migrations/sql/0147-add-upload-session-table.sql")}])
|
||||||
|
|
||||||
|
|||||||
@ -1,13 +0,0 @@
|
|||||||
ALTER TABLE team_invitation
|
|
||||||
ADD COLUMN org_id uuid NULL;
|
|
||||||
|
|
||||||
ALTER TABLE team_invitation
|
|
||||||
ALTER COLUMN team_id DROP NOT NULL;
|
|
||||||
|
|
||||||
ALTER TABLE team_invitation
|
|
||||||
ADD CONSTRAINT team_invitation_team_or_org_not_null
|
|
||||||
CHECK (team_id IS NOT NULL OR org_id IS NOT NULL);
|
|
||||||
|
|
||||||
CREATE UNIQUE INDEX team_invitation_org_unique
|
|
||||||
ON team_invitation (org_id, email_to)
|
|
||||||
WHERE team_id IS NULL;
|
|
||||||
@ -1,23 +1,15 @@
|
|||||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
||||||
;;
|
|
||||||
;; Copyright (c) KALEIDOS INC
|
|
||||||
|
|
||||||
(ns app.nitrate
|
(ns app.nitrate
|
||||||
"Module that make calls to the external nitrate aplication"
|
"Module that make calls to the external nitrate aplication"
|
||||||
(:require
|
(:require
|
||||||
[app.common.exceptions :as ex]
|
|
||||||
[app.common.json :as json]
|
|
||||||
[app.common.logging :as l]
|
[app.common.logging :as l]
|
||||||
[app.common.schema :as sm]
|
[app.common.schema :as sm]
|
||||||
[app.common.schema.generators :as sg]
|
[app.common.schema.generators :as sg]
|
||||||
[app.common.time :as ct]
|
[app.common.time :as ct]
|
||||||
[app.common.types.organization :as cto]
|
|
||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
[app.http.client :as http]
|
[app.http.client :as http]
|
||||||
[app.rpc :as-alias rpc]
|
[app.rpc :as-alias rpc]
|
||||||
[app.setup :as-alias setup]
|
[app.setup :as-alias setup]
|
||||||
|
[app.util.json :as json]
|
||||||
[clojure.core :as c]
|
[clojure.core :as c]
|
||||||
[integrant.core :as ig]))
|
[integrant.core :as ig]))
|
||||||
|
|
||||||
@ -26,16 +18,16 @@
|
|||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
(defn- request-builder
|
(defn- request-builder
|
||||||
[cfg method uri shared-key profile-id request-params]
|
[cfg method uri shared-key profile-id]
|
||||||
(fn []
|
(fn []
|
||||||
(http/req! cfg (cond-> {:method method
|
(http/req! cfg {:method method
|
||||||
:headers {"content-type" "application/json"
|
:headers {"content-type" "application/json"
|
||||||
"accept" "application/json"
|
"accept" "application/json"
|
||||||
"x-shared-key" shared-key
|
"x-shared-key" shared-key
|
||||||
"x-profile-id" (str profile-id)}
|
"x-profile-id" (str profile-id)}
|
||||||
:uri uri
|
:uri uri
|
||||||
:version :http1.1}
|
:version :http1.1})))
|
||||||
(= method :post) (assoc :body (json/encode request-params :key-fn json/write-camel-key))))))
|
|
||||||
|
|
||||||
(defn- with-retries
|
(defn- with-retries
|
||||||
[handler max-retries]
|
[handler max-retries]
|
||||||
@ -57,41 +49,20 @@
|
|||||||
|
|
||||||
(defn- with-validate [handler uri schema]
|
(defn- with-validate [handler uri schema]
|
||||||
(fn []
|
(fn []
|
||||||
(let [response (handler)
|
(let [coercer-http (sm/coercer schema
|
||||||
status (:status response)]
|
:type :validation
|
||||||
(when-not status
|
:hint (str "invalid data received calling " uri))]
|
||||||
(l/error :hint "could't do the nitrate request, it is probably down"
|
(try
|
||||||
:uri uri)
|
(coercer-http (-> (handler) :body json/decode))
|
||||||
;; TODO decide what to do when Nitrate is inaccesible
|
(catch Exception e
|
||||||
nil)
|
;; TODO Error handling
|
||||||
(cond
|
(l/error :hint "error validating json response" :cause e)
|
||||||
(>= status 400)
|
nil)))))
|
||||||
;; For error status codes (4xx, 5xx), fail immediately without validation
|
|
||||||
(do
|
|
||||||
(when (not= status 404) ;; Don't need to log 404
|
|
||||||
(l/error :hint "nitrate request failed with error status"
|
|
||||||
:uri uri
|
|
||||||
:status status
|
|
||||||
:body (:body response)))
|
|
||||||
nil)
|
|
||||||
(= status 204) ;; 204 doesn't return any body
|
|
||||||
nil
|
|
||||||
:else ;; For success status codes, validate the response
|
|
||||||
(let [coercer-http (sm/coercer schema
|
|
||||||
:type :validation
|
|
||||||
:hint (str "invalid data received calling " uri))
|
|
||||||
data (-> response :body (json/decode :key-fn json/read-kebab-key))]
|
|
||||||
(try
|
|
||||||
(coercer-http data)
|
|
||||||
(catch Exception e
|
|
||||||
;; TODO Error handling
|
|
||||||
(l/error :hint "error validating json response" :cause e)
|
|
||||||
nil)))))))
|
|
||||||
|
|
||||||
(defn- request-to-nitrate
|
(defn- request-to-nitrate
|
||||||
[cfg method uri schema {:keys [::rpc/profile-id request-params] :as params}]
|
[cfg method uri schema {:keys [::rpc/profile-id] :as params}]
|
||||||
(let [shared-key (-> cfg ::setup/shared-keys :nitrate)
|
(let [shared-key (-> cfg ::setup/shared-keys :nitrate)
|
||||||
full-http-call (-> (request-builder cfg method uri shared-key profile-id request-params)
|
full-http-call (-> (request-builder cfg method uri shared-key profile-id)
|
||||||
(with-retries 3)
|
(with-retries 3)
|
||||||
(with-validate uri schema))]
|
(with-validate uri schema))]
|
||||||
(full-http-call)))
|
(full-http-call)))
|
||||||
@ -109,23 +80,11 @@
|
|||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
(def ^:private schema:org-summary
|
(def ^:private schema:organization
|
||||||
[:map
|
[:map
|
||||||
[:id ::sm/uuid]
|
[:id ::sm/uuid]
|
||||||
[:name ::sm/text]
|
[:name ::sm/text]
|
||||||
[:owner-id ::sm/uuid]
|
[:slug ::sm/text]])
|
||||||
[:teams
|
|
||||||
[:vector
|
|
||||||
[:map
|
|
||||||
[:id ::sm/uuid]
|
|
||||||
[:is-your-penpot :boolean]]]]])
|
|
||||||
|
|
||||||
(def ^:private schema:profile-org
|
|
||||||
[:map
|
|
||||||
[:is-member :boolean]
|
|
||||||
[:organization-id {:optional true} [:maybe ::sm/uuid]]
|
|
||||||
[:default-team-id {:optional true} [:maybe ::sm/uuid]]])
|
|
||||||
|
|
||||||
|
|
||||||
;; TODO Unify with schemas on backend/src/app/http/management.clj
|
;; TODO Unify with schemas on backend/src/app/http/management.clj
|
||||||
(def ^:private schema:timestamp
|
(def ^:private schema:timestamp
|
||||||
@ -199,136 +158,20 @@
|
|||||||
[:map
|
[:map
|
||||||
[:licenses ::sm/boolean]])
|
[:licenses ::sm/boolean]])
|
||||||
|
|
||||||
(defn- get-team-org-api
|
(defn- get-team-org
|
||||||
[cfg {:keys [team-id] :as params}]
|
[cfg {:keys [team-id] :as params}]
|
||||||
(let [baseuri (cf/get :nitrate-backend-uri)]
|
(let [baseuri (cf/get :nitrate-backend-uri)]
|
||||||
(request-to-nitrate cfg :get
|
(request-to-nitrate cfg :get (str baseuri "/api/teams/" (str team-id)) schema:organization params)))
|
||||||
(str baseuri
|
|
||||||
"/api/teams/"
|
|
||||||
team-id)
|
|
||||||
cto/schema:team-with-organization params)))
|
|
||||||
|
|
||||||
(defn- get-org-membership-api
|
(defn- get-subscription
|
||||||
[cfg {:keys [profile-id organization-id] :as params}]
|
|
||||||
(let [baseuri (cf/get :nitrate-backend-uri)]
|
|
||||||
(request-to-nitrate cfg :get
|
|
||||||
(str baseuri
|
|
||||||
"/api/organizations/"
|
|
||||||
organization-id
|
|
||||||
"/members/"
|
|
||||||
profile-id)
|
|
||||||
schema:profile-org params)))
|
|
||||||
|
|
||||||
(defn- get-org-membership-by-team-api
|
|
||||||
[cfg {:keys [profile-id team-id] :as params}]
|
|
||||||
(let [baseuri (cf/get :nitrate-backend-uri)]
|
|
||||||
(request-to-nitrate cfg :get
|
|
||||||
(str baseuri
|
|
||||||
"/api/teams/"
|
|
||||||
team-id
|
|
||||||
"/users/"
|
|
||||||
profile-id)
|
|
||||||
schema:profile-org params)))
|
|
||||||
|
|
||||||
|
|
||||||
(defn- get-org-summary-api
|
|
||||||
[cfg {:keys [organization-id] :as params}]
|
|
||||||
(let [baseuri (cf/get :nitrate-backend-uri)]
|
|
||||||
(request-to-nitrate cfg :get
|
|
||||||
(str baseuri
|
|
||||||
"/api/organizations/"
|
|
||||||
organization-id
|
|
||||||
"/summary")
|
|
||||||
schema:org-summary params)))
|
|
||||||
|
|
||||||
|
|
||||||
(defn- set-team-org-api
|
|
||||||
[cfg {:keys [organization-id team-id is-default] :as params}]
|
|
||||||
(let [baseuri (cf/get :nitrate-backend-uri)
|
|
||||||
params (assoc params :request-params {:team-id team-id
|
|
||||||
:is-your-penpot (true? is-default)})
|
|
||||||
team (request-to-nitrate cfg :post
|
|
||||||
(str baseuri
|
|
||||||
"/api/organizations/"
|
|
||||||
organization-id
|
|
||||||
"/add-team")
|
|
||||||
cto/schema:team-with-organization params)
|
|
||||||
custom-photo (when-let [logo-id (get-in team [:organization :logo-id])]
|
|
||||||
(str (cf/get :public-uri) "/assets/by-id/" logo-id))]
|
|
||||||
(cond-> team
|
|
||||||
custom-photo
|
|
||||||
(assoc-in [:organization :custom-photo] custom-photo))))
|
|
||||||
|
|
||||||
(defn- add-profile-to-org-api
|
|
||||||
[cfg {:keys [profile-id organization-id team-id email] :as params}]
|
|
||||||
(let [baseuri (cf/get :nitrate-backend-uri)
|
|
||||||
request-params (cond-> {:user-id profile-id :team-id team-id}
|
|
||||||
(some? email) (assoc :email email))
|
|
||||||
params (assoc params :request-params request-params)]
|
|
||||||
(request-to-nitrate cfg :post
|
|
||||||
(str baseuri
|
|
||||||
"/api/organizations/"
|
|
||||||
organization-id
|
|
||||||
"/add-user")
|
|
||||||
schema:profile-org params)))
|
|
||||||
|
|
||||||
(defn- remove-profile-from-org-api
|
|
||||||
[cfg {:keys [profile-id organization-id] :as params}]
|
|
||||||
(let [baseuri (cf/get :nitrate-backend-uri)
|
|
||||||
params (assoc params :request-params {:user-id profile-id})]
|
|
||||||
(request-to-nitrate cfg :post
|
|
||||||
(str baseuri
|
|
||||||
"/api/organizations/"
|
|
||||||
organization-id
|
|
||||||
"/remove-user")
|
|
||||||
nil params)))
|
|
||||||
|
|
||||||
(defn- remove-profile-from-all-orgs-api
|
|
||||||
[cfg {:keys [profile-id] :as params}]
|
[cfg {:keys [profile-id] :as params}]
|
||||||
(let [baseuri (cf/get :nitrate-backend-uri)]
|
(let [baseuri (cf/get :nitrate-backend-uri)]
|
||||||
(request-to-nitrate cfg :post
|
(request-to-nitrate cfg :get (str baseuri "/api/subscriptions/" (str profile-id)) schema:subscription params)))
|
||||||
(str baseuri
|
|
||||||
"/api/users/"
|
|
||||||
profile-id
|
|
||||||
"/remove-organizations")
|
|
||||||
nil params)))
|
|
||||||
|
|
||||||
(defn- remove-team-from-org-api
|
(defn- get-connectivity
|
||||||
[cfg {:keys [team-id organization-id] :as params}]
|
|
||||||
(let [baseuri (cf/get :nitrate-backend-uri)
|
|
||||||
params (assoc params :request-params {:team-id team-id})]
|
|
||||||
(request-to-nitrate cfg :post
|
|
||||||
(str baseuri
|
|
||||||
"/api/organizations/"
|
|
||||||
organization-id
|
|
||||||
"/remove-team")
|
|
||||||
nil params)))
|
|
||||||
|
|
||||||
(defn- delete-team-api
|
|
||||||
[cfg {:keys [team-id] :as params}]
|
|
||||||
(let [baseuri (cf/get :nitrate-backend-uri)]
|
|
||||||
(request-to-nitrate cfg :delete
|
|
||||||
(str baseuri
|
|
||||||
"/api/teams/"
|
|
||||||
team-id)
|
|
||||||
nil params)))
|
|
||||||
|
|
||||||
(defn- get-subscription-api
|
|
||||||
[cfg {:keys [profile-id] :as params}]
|
|
||||||
(let [baseuri (cf/get :nitrate-backend-uri)]
|
|
||||||
(request-to-nitrate cfg :get
|
|
||||||
(str baseuri
|
|
||||||
"/api/subscriptions/"
|
|
||||||
profile-id)
|
|
||||||
schema:subscription params)))
|
|
||||||
|
|
||||||
(defn- get-connectivity-api
|
|
||||||
[cfg params]
|
[cfg params]
|
||||||
(let [baseuri (cf/get :nitrate-backend-uri)]
|
(let [baseuri (cf/get :nitrate-backend-uri)]
|
||||||
(request-to-nitrate cfg :get
|
(request-to-nitrate cfg :get (str baseuri "/api/connectivity") schema:connectivity params)))
|
||||||
(str baseuri
|
|
||||||
"/api/connectivity")
|
|
||||||
schema:connectivity params)))
|
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; INITIALIZATION
|
;; INITIALIZATION
|
||||||
@ -337,18 +180,9 @@
|
|||||||
(defmethod ig/init-key ::client
|
(defmethod ig/init-key ::client
|
||||||
[_ cfg]
|
[_ cfg]
|
||||||
(when (contains? cf/flags :nitrate)
|
(when (contains? cf/flags :nitrate)
|
||||||
{:get-team-org (partial get-team-org-api cfg)
|
{:get-team-org (partial get-team-org cfg)
|
||||||
:set-team-org (partial set-team-org-api cfg)
|
:get-subscription (partial get-subscription cfg)
|
||||||
:get-org-membership (partial get-org-membership-api cfg)
|
:connectivity (partial get-connectivity cfg)}))
|
||||||
:get-org-membership-by-team (partial get-org-membership-by-team-api cfg)
|
|
||||||
:get-org-summary (partial get-org-summary-api cfg)
|
|
||||||
:add-profile-to-org (partial add-profile-to-org-api cfg)
|
|
||||||
:remove-profile-from-org (partial remove-profile-from-org-api cfg)
|
|
||||||
:remove-profile-from-all-orgs (partial remove-profile-from-all-orgs-api cfg)
|
|
||||||
:delete-team (partial delete-team-api cfg)
|
|
||||||
:remove-team-from-org (partial remove-team-from-org-api cfg)
|
|
||||||
:get-subscription (partial get-subscription-api cfg)
|
|
||||||
:connectivity (partial get-connectivity-api cfg)}))
|
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; UTILS
|
;; UTILS
|
||||||
@ -371,18 +205,18 @@
|
|||||||
|
|
||||||
(defn add-org-info-to-team
|
(defn add-org-info-to-team
|
||||||
"Enriches a team map with organization information from Nitrate.
|
"Enriches a team map with organization information from Nitrate.
|
||||||
Adds organization-id, organization-name, organization-slug, organization-owner-id, and your-penpot fields.
|
Adds organization-id, organization-name, organization-slug, and your-penpot fields.
|
||||||
Returns the original team unchanged if the request fails or org data is nil."
|
Returns the original team unchanged if the request fails or org data is nil."
|
||||||
[cfg team params]
|
[cfg team params]
|
||||||
(try
|
(try
|
||||||
(let [params (assoc (or params {}) :team-id (:id team))
|
(let [params (assoc (or params {}) :team-id (:id team))
|
||||||
team-with-org (call cfg :get-team-org params)
|
org (call cfg :get-team-org params)]
|
||||||
org (:organization team-with-org)]
|
|
||||||
(if (some? org)
|
(if (some? org)
|
||||||
(-> (cto/apply-organization team (assoc org :custom-photo
|
(assoc team
|
||||||
(when-let [logo-id (:logo-id org)]
|
:organization-id (:id org)
|
||||||
(str (cf/get :public-uri) "/assets/by-id/" logo-id))))
|
:organization-name (:name org)
|
||||||
(assoc :is-default (or (:is-default team) (true? (:is-your-penpot team-with-org)))))
|
:organization-slug (:slug org)
|
||||||
|
:is-default (or (:is-default team) (true? (:isYourPenpot org))))
|
||||||
team))
|
team))
|
||||||
(catch Throwable cause
|
(catch Throwable cause
|
||||||
(l/error :hint "failed to get team organization info"
|
(l/error :hint "failed to get team organization info"
|
||||||
@ -390,23 +224,6 @@
|
|||||||
:cause cause)
|
:cause cause)
|
||||||
team)))
|
team)))
|
||||||
|
|
||||||
(defn set-team-organization
|
(defn connectivity
|
||||||
"Associates a team with an organization in Nitrate.
|
[cfg]
|
||||||
Requires organization-id and is-default in params.
|
(call cfg :connectivity {}))
|
||||||
Throws an exception if the request fails."
|
|
||||||
[cfg team params]
|
|
||||||
(let [params (assoc (or params {})
|
|
||||||
:team-id (:id team)
|
|
||||||
:organization-id (:organization-id params)
|
|
||||||
:is-default (:is-default params))
|
|
||||||
result (call cfg :set-team-org params)]
|
|
||||||
(when (nil? result)
|
|
||||||
(ex/raise :type :internal
|
|
||||||
:code :failed-to-set-team-org
|
|
||||||
:context {:team-id (:id team)
|
|
||||||
:organization-id (:organization-id params)}))
|
|
||||||
team))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -24,28 +24,28 @@
|
|||||||
[integrant.core :as ig])
|
[integrant.core :as ig])
|
||||||
(:import
|
(:import
|
||||||
clojure.lang.MapEntry
|
clojure.lang.MapEntry
|
||||||
io.lettuce.core.api.StatefulRedisConnection
|
|
||||||
io.lettuce.core.api.sync.RedisCommands
|
|
||||||
io.lettuce.core.api.sync.RedisScriptingCommands
|
|
||||||
io.lettuce.core.codec.RedisCodec
|
|
||||||
io.lettuce.core.codec.StringCodec
|
|
||||||
io.lettuce.core.KeyValue
|
io.lettuce.core.KeyValue
|
||||||
io.lettuce.core.pubsub.api.sync.RedisPubSubCommands
|
|
||||||
io.lettuce.core.pubsub.RedisPubSubListener
|
|
||||||
io.lettuce.core.pubsub.StatefulRedisPubSubConnection
|
|
||||||
io.lettuce.core.RedisClient
|
io.lettuce.core.RedisClient
|
||||||
io.lettuce.core.RedisCommandInterruptedException
|
io.lettuce.core.RedisCommandInterruptedException
|
||||||
io.lettuce.core.RedisCommandTimeoutException
|
io.lettuce.core.RedisCommandTimeoutException
|
||||||
io.lettuce.core.RedisException
|
io.lettuce.core.RedisException
|
||||||
io.lettuce.core.RedisURI
|
io.lettuce.core.RedisURI
|
||||||
io.lettuce.core.resource.ClientResources
|
|
||||||
io.lettuce.core.resource.DefaultClientResources
|
|
||||||
io.lettuce.core.ScriptOutputType
|
io.lettuce.core.ScriptOutputType
|
||||||
io.lettuce.core.SetArgs
|
io.lettuce.core.SetArgs
|
||||||
|
io.lettuce.core.api.StatefulRedisConnection
|
||||||
|
io.lettuce.core.api.sync.RedisCommands
|
||||||
|
io.lettuce.core.api.sync.RedisScriptingCommands
|
||||||
|
io.lettuce.core.codec.RedisCodec
|
||||||
|
io.lettuce.core.codec.StringCodec
|
||||||
|
io.lettuce.core.pubsub.RedisPubSubListener
|
||||||
|
io.lettuce.core.pubsub.StatefulRedisPubSubConnection
|
||||||
|
io.lettuce.core.pubsub.api.sync.RedisPubSubCommands
|
||||||
|
io.lettuce.core.resource.ClientResources
|
||||||
|
io.lettuce.core.resource.DefaultClientResources
|
||||||
io.netty.channel.nio.NioEventLoopGroup
|
io.netty.channel.nio.NioEventLoopGroup
|
||||||
io.netty.util.concurrent.EventExecutorGroup
|
|
||||||
io.netty.util.HashedWheelTimer
|
io.netty.util.HashedWheelTimer
|
||||||
io.netty.util.Timer
|
io.netty.util.Timer
|
||||||
|
io.netty.util.concurrent.EventExecutorGroup
|
||||||
java.lang.AutoCloseable
|
java.lang.AutoCloseable
|
||||||
java.time.Duration))
|
java.time.Duration))
|
||||||
|
|
||||||
|
|||||||
@ -372,11 +372,9 @@
|
|||||||
(throw cause))))))
|
(throw cause))))))
|
||||||
|
|
||||||
(defn create-profile-rels
|
(defn create-profile-rels
|
||||||
[{:keys [::db/conn] :as cfg} {:keys [id] :as profile}]
|
[conn {:keys [id] :as profile}]
|
||||||
(assert (db/connection-map? cfg)
|
|
||||||
"expected cfg with valid connection")
|
|
||||||
(let [features (cfeat/get-enabled-features cf/flags)
|
(let [features (cfeat/get-enabled-features cf/flags)
|
||||||
team (teams/create-team cfg
|
team (teams/create-team conn
|
||||||
{:profile-id id
|
{:profile-id id
|
||||||
:name "Default"
|
:name "Default"
|
||||||
:features features
|
:features features
|
||||||
@ -431,7 +429,7 @@
|
|||||||
(assoc :is-active is-active)
|
(assoc :is-active is-active)
|
||||||
(update :password auth/derive-password))
|
(update :password auth/derive-password))
|
||||||
profile (->> (create-profile cfg params)
|
profile (->> (create-profile cfg params)
|
||||||
(create-profile-rels cfg))]
|
(create-profile-rels conn))]
|
||||||
(vary-meta profile assoc :created true))))
|
(vary-meta profile assoc :created true))))
|
||||||
|
|
||||||
created? (-> profile meta :created true?)
|
created? (-> profile meta :created true?)
|
||||||
|
|||||||
@ -49,9 +49,9 @@
|
|||||||
:deleted-at (ct/in-future (cf/get-deletion-delay))
|
:deleted-at (ct/in-future (cf/get-deletion-delay))
|
||||||
:password (derive-password password)
|
:password (derive-password password)
|
||||||
:props {}}
|
:props {}}
|
||||||
profile (db/tx-run! cfg (fn [cfg]
|
profile (db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
|
||||||
(->> (auth/create-profile cfg params)
|
(->> (auth/create-profile cfg params)
|
||||||
(auth/create-profile-rels cfg))))]
|
(auth/create-profile-rels conn))))]
|
||||||
(with-meta {:email email
|
(with-meta {:email email
|
||||||
:password password}
|
:password password}
|
||||||
{::audit/profile-id (:id profile)})))
|
{::audit/profile-id (:id profile)})))
|
||||||
|
|||||||
@ -13,7 +13,6 @@
|
|||||||
[app.common.features :as cfeat]
|
[app.common.features :as cfeat]
|
||||||
[app.common.files.helpers :as cfh]
|
[app.common.files.helpers :as cfh]
|
||||||
[app.common.files.migrations :as fmg]
|
[app.common.files.migrations :as fmg]
|
||||||
[app.common.files.stats :as cfs]
|
|
||||||
[app.common.logging :as l]
|
[app.common.logging :as l]
|
||||||
[app.common.schema :as sm]
|
[app.common.schema :as sm]
|
||||||
[app.common.schema.desc-js-like :as-alias smdj]
|
[app.common.schema.desc-js-like :as-alias smdj]
|
||||||
@ -607,76 +606,6 @@
|
|||||||
(get-file-summary cfg id))
|
(get-file-summary cfg id))
|
||||||
|
|
||||||
|
|
||||||
;; --- COMMAND QUERY: get-file-stats
|
|
||||||
|
|
||||||
(def ^:private sql:file-stats-library-counts
|
|
||||||
"SELECT
|
|
||||||
(SELECT COUNT(*)
|
|
||||||
FROM file_library_rel AS flr
|
|
||||||
JOIN file AS fl ON (fl.id = flr.library_file_id)
|
|
||||||
WHERE flr.file_id = ?::uuid
|
|
||||||
AND (fl.deleted_at IS NULL OR fl.deleted_at > now())) AS library_count,
|
|
||||||
(SELECT COUNT(*)
|
|
||||||
FROM file_library_rel AS flr
|
|
||||||
JOIN file AS fl ON (fl.id = flr.file_id)
|
|
||||||
WHERE flr.library_file_id = ?::uuid
|
|
||||||
AND (fl.deleted_at IS NULL OR fl.deleted_at > now())) AS referenced_by_count")
|
|
||||||
|
|
||||||
(defn- get-file-stats-library-counts
|
|
||||||
[conn file-id]
|
|
||||||
(let [row (db/exec-one! conn [sql:file-stats-library-counts file-id file-id])]
|
|
||||||
{:library-count (or (:library-count row) 0)
|
|
||||||
:referenced-by-count (or (:referenced-by-count row) 0)}))
|
|
||||||
|
|
||||||
(defn- get-file-stats
|
|
||||||
[{:keys [::db/conn] :as cfg} file-id]
|
|
||||||
(let [file (bfc/get-file cfg file-id)
|
|
||||||
base (binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg file-id)]
|
|
||||||
(cfs/calc-file-stats (:data file)))
|
|
||||||
lib-cnt (get-file-stats-library-counts conn file-id)]
|
|
||||||
(-> base
|
|
||||||
(merge lib-cnt)
|
|
||||||
(assoc :file-id file-id
|
|
||||||
:revn (:revn file)
|
|
||||||
:updated-at (:modified-at file)))))
|
|
||||||
|
|
||||||
(def ^:private schema:shape-counts
|
|
||||||
[:map {:title "FileStatsShapeCounts"}
|
|
||||||
[:total [::sm/int {:min 0}]]
|
|
||||||
[:by-type [:map-of :keyword [::sm/int {:min 0}]]]])
|
|
||||||
|
|
||||||
(def ^:private schema:get-file-stats-result
|
|
||||||
[:map {:title "FileStats"}
|
|
||||||
[:file-id ::sm/uuid]
|
|
||||||
[:page-count [::sm/int {:min 0}]]
|
|
||||||
[:shape-counts schema:shape-counts]
|
|
||||||
[:component-count [::sm/int {:min 0}]]
|
|
||||||
[:deleted-component-count [::sm/int {:min 0}]]
|
|
||||||
[:color-count [::sm/int {:min 0}]]
|
|
||||||
[:typography-count [::sm/int {:min 0}]]
|
|
||||||
[:library-count [::sm/int {:min 0}]]
|
|
||||||
[:referenced-by-count [::sm/int {:min 0}]]
|
|
||||||
[:revn [::sm/int {:min 0}]]
|
|
||||||
[:updated-at ::ct/inst]])
|
|
||||||
|
|
||||||
(def ^:private schema:get-file-stats
|
|
||||||
[:map {:title "get-file-stats"}
|
|
||||||
[:id ::sm/uuid]])
|
|
||||||
|
|
||||||
(sv/defmethod ::get-file-stats
|
|
||||||
"Return aggregate statistics for a single file: page count, shape
|
|
||||||
counts by type, component/color/typography counts, and inbound and
|
|
||||||
outbound library reference counts. Cheap alternative to `get-file`
|
|
||||||
when only metrics are needed."
|
|
||||||
{::doc/added "2.17"
|
|
||||||
::sm/params schema:get-file-stats
|
|
||||||
::sm/result schema:get-file-stats-result
|
|
||||||
::db/transaction true}
|
|
||||||
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id id]}]
|
|
||||||
(check-read-permissions! conn profile-id id)
|
|
||||||
(get-file-stats cfg id))
|
|
||||||
|
|
||||||
|
|
||||||
;; --- COMMAND QUERY: get-file-libraries
|
;; --- COMMAND QUERY: get-file-libraries
|
||||||
|
|
||||||
(def ^:private schema:get-file-libraries
|
(def ^:private schema:get-file-libraries
|
||||||
|
|||||||
@ -8,7 +8,6 @@
|
|||||||
(:require
|
(:require
|
||||||
[app.binfile.common :as bfc]
|
[app.binfile.common :as bfc]
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
[app.common.features :as-alias cfeat]
|
|
||||||
[app.common.schema :as sm]
|
[app.common.schema :as sm]
|
||||||
[app.common.time :as ct]
|
[app.common.time :as ct]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
@ -36,43 +35,6 @@
|
|||||||
(files/check-read-permissions! conn profile-id file-id)
|
(files/check-read-permissions! conn profile-id file-id)
|
||||||
(fsnap/get-visible-snapshots conn file-id))))
|
(fsnap/get-visible-snapshots conn file-id))))
|
||||||
|
|
||||||
;; --- COMMAND QUERY: get-file-snapshot
|
|
||||||
|
|
||||||
(def ^:private schema:get-file-snapshot
|
|
||||||
[:map {:title "get-file-snapshot"}
|
|
||||||
[:file-id ::sm/uuid]
|
|
||||||
[:id ::sm/uuid]
|
|
||||||
[:features {:optional true} ::cfeat/features]])
|
|
||||||
|
|
||||||
(sv/defmethod ::get-file-snapshot
|
|
||||||
"Retrieve a file bundle with data from a specific snapshot for
|
|
||||||
read-only preview. Does not modify any database state."
|
|
||||||
{::doc/added "2.16"
|
|
||||||
::sm/params schema:get-file-snapshot
|
|
||||||
::sm/result files/schema:file-with-permissions
|
|
||||||
::db/transaction true}
|
|
||||||
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id file-id id] :as params}]
|
|
||||||
(let [perms (bfc/get-file-permissions conn profile-id file-id)]
|
|
||||||
(files/check-read-permissions! perms)
|
|
||||||
(let [snapshot (fsnap/get-snapshot-data cfg file-id id)]
|
|
||||||
(when-not snapshot
|
|
||||||
(ex/raise :type :not-found
|
|
||||||
:code :snapshot-not-found
|
|
||||||
:hint "unable to find snapshot with the provided id"
|
|
||||||
:snapshot-id id
|
|
||||||
:file-id file-id))
|
|
||||||
;; Load current file metadata only (no data decoding) then overlay
|
|
||||||
;; the snapshot data so the client receives the same shape as a
|
|
||||||
;; normal get-file response but with historical page/object content.
|
|
||||||
(let [base-file (bfc/get-file cfg file-id :load-data? false)]
|
|
||||||
(-> base-file
|
|
||||||
(assoc :data (:data snapshot))
|
|
||||||
(assoc :version (:version snapshot))
|
|
||||||
(assoc :features (:features snapshot))
|
|
||||||
(assoc :revn (:revn snapshot))
|
|
||||||
(assoc :vern (rand-int 100000))
|
|
||||||
(assoc :permissions perms))))))
|
|
||||||
|
|
||||||
(def ^:private schema:create-file-snapshot
|
(def ^:private schema:create-file-snapshot
|
||||||
[:map
|
[:map
|
||||||
[:file-id ::sm/uuid]
|
[:file-id ::sm/uuid]
|
||||||
|
|||||||
@ -84,5 +84,5 @@
|
|||||||
(profile/get-profile-by-email conn))
|
(profile/get-profile-by-email conn))
|
||||||
(->> (assoc info :is-active true :is-demo false)
|
(->> (assoc info :is-active true :is-demo false)
|
||||||
(auth/create-profile cfg)
|
(auth/create-profile cfg)
|
||||||
(auth/create-profile-rels cfg)
|
(auth/create-profile-rels conn)
|
||||||
(profile/strip-private-attrs))))))
|
(profile/strip-private-attrs))))))
|
||||||
|
|||||||
@ -207,7 +207,8 @@
|
|||||||
(update :team-id bfc/lookup-index)
|
(update :team-id bfc/lookup-index)
|
||||||
(assoc :created-at timestamp)
|
(assoc :created-at timestamp)
|
||||||
(assoc :modified-at timestamp))]
|
(assoc :modified-at timestamp))]
|
||||||
(teams/add-profile-to-team! cfg params {::db/return-keys false})))
|
(db/insert! conn :team-profile-rel params
|
||||||
|
{::db/return-keys false})))
|
||||||
|
|
||||||
;; Duplicate team fonts
|
;; Duplicate team fonts
|
||||||
(doseq [font fonts]
|
(doseq [font fonts]
|
||||||
@ -338,21 +339,6 @@
|
|||||||
;; --- COMMAND: Move project
|
;; --- COMMAND: Move project
|
||||||
|
|
||||||
(defn move-project
|
(defn move-project
|
||||||
"Moves a project from one team to another.
|
|
||||||
|
|
||||||
Performs comprehensive validation including:
|
|
||||||
- Permission checks on both source and destination teams
|
|
||||||
- Team compatibility verification between source and destination
|
|
||||||
- File features compatibility with destination team
|
|
||||||
|
|
||||||
The operation also:
|
|
||||||
- Updates the project's team assignment
|
|
||||||
- Cleans up any broken library relations after the move
|
|
||||||
|
|
||||||
Throws:
|
|
||||||
- :cant-move-to-same-team if trying to move project to its current team
|
|
||||||
- Permission exceptions if user lacks required permissions
|
|
||||||
- Team compatibility exceptions if teams are incompatible"
|
|
||||||
[{:keys [::db/conn] :as cfg} {:keys [profile-id team-id project-id] :as params}]
|
[{:keys [::db/conn] :as cfg} {:keys [profile-id team-id project-id] :as params}]
|
||||||
(let [project (db/get-by-id conn :project project-id {:columns [:id :team-id]})
|
(let [project (db/get-by-id conn :project project-id {:columns [:id :team-id]})
|
||||||
pids (->> (db/query conn :project {:team-id (:team-id project)} {:columns [:id]})
|
pids (->> (db/query conn :project {:team-id (:team-id project)} {:columns [:id]})
|
||||||
|
|||||||
@ -255,7 +255,7 @@
|
|||||||
[:session-id ::sm/uuid]])
|
[:session-id ::sm/uuid]])
|
||||||
|
|
||||||
(sv/defmethod ::create-upload-session
|
(sv/defmethod ::create-upload-session
|
||||||
{::doc/added "2.17"
|
{::doc/added "2.16"
|
||||||
::sm/params schema:create-upload-session
|
::sm/params schema:create-upload-session
|
||||||
::sm/result schema:create-upload-session-result}
|
::sm/result schema:create-upload-session-result}
|
||||||
[{:keys [::db/pool] :as cfg}
|
[{:keys [::db/pool] :as cfg}
|
||||||
@ -293,7 +293,7 @@
|
|||||||
[:index ::sm/int]])
|
[:index ::sm/int]])
|
||||||
|
|
||||||
(sv/defmethod ::upload-chunk
|
(sv/defmethod ::upload-chunk
|
||||||
{::doc/added "2.17"
|
{::doc/added "2.16"
|
||||||
::sm/params schema:upload-chunk
|
::sm/params schema:upload-chunk
|
||||||
::sm/result schema:upload-chunk-result}
|
::sm/result schema:upload-chunk-result}
|
||||||
[{:keys [::db/pool] :as cfg}
|
[{:keys [::db/pool] :as cfg}
|
||||||
@ -389,7 +389,7 @@
|
|||||||
[:id {:optional true} ::sm/uuid]])
|
[:id {:optional true} ::sm/uuid]])
|
||||||
|
|
||||||
(sv/defmethod ::assemble-file-media-object
|
(sv/defmethod ::assemble-file-media-object
|
||||||
{::doc/added "2.17"
|
{::doc/added "2.16"
|
||||||
::sm/params schema:assemble-file-media-object
|
::sm/params schema:assemble-file-media-object
|
||||||
::climit/id [[:process-image/by-profile ::rpc/profile-id]
|
::climit/id [[:process-image/by-profile ::rpc/profile-id]
|
||||||
[:process-image/global]]}
|
[:process-image/global]]}
|
||||||
|
|||||||
@ -1,283 +1,20 @@
|
|||||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
||||||
;;
|
|
||||||
;; Copyright (c) KALEIDOS INC
|
|
||||||
|
|
||||||
(ns app.rpc.commands.nitrate
|
(ns app.rpc.commands.nitrate
|
||||||
"Nitrate API for Penpot. Provides nitrate-related endpoints to be called
|
|
||||||
from Penpot frontend."
|
|
||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
|
||||||
[app.common.exceptions :as ex]
|
|
||||||
[app.common.schema :as sm]
|
[app.common.schema :as sm]
|
||||||
[app.db :as db]
|
|
||||||
[app.nitrate :as nitrate]
|
[app.nitrate :as nitrate]
|
||||||
[app.rpc :as-alias rpc]
|
[app.rpc :as-alias rpc]
|
||||||
[app.rpc.commands.teams :as teams]
|
|
||||||
[app.rpc.doc :as-alias doc]
|
[app.rpc.doc :as-alias doc]
|
||||||
[app.rpc.notifications :as notifications]
|
|
||||||
[app.util.services :as sv]))
|
[app.util.services :as sv]))
|
||||||
|
|
||||||
|
|
||||||
(defn assert-is-owner [cfg profile-id team-id]
|
|
||||||
(let [perms (teams/get-permissions cfg profile-id team-id)]
|
|
||||||
(when-not (:is-owner perms)
|
|
||||||
(ex/raise :type :validation
|
|
||||||
:code :insufficient-permissions))))
|
|
||||||
|
|
||||||
(defn assert-not-default-team [cfg team-id]
|
|
||||||
(let [team (teams/get-team-info cfg {:id team-id})]
|
|
||||||
(when (:is-default team)
|
|
||||||
(ex/raise :type :validation
|
|
||||||
:code :cant-move-default-team))))
|
|
||||||
|
|
||||||
(defn assert-membership [cfg profile-id organization-id]
|
|
||||||
(let [membership (nitrate/call cfg :get-org-membership {:profile-id profile-id
|
|
||||||
:organization-id organization-id})]
|
|
||||||
(when-not (:organization-id membership)
|
|
||||||
(ex/raise :type :validation
|
|
||||||
:code :organization-doesnt-exists))
|
|
||||||
|
|
||||||
(when-not (:is-member membership)
|
|
||||||
(ex/raise :type :validation
|
|
||||||
:code :user-doesnt-belong-organization))))
|
|
||||||
|
|
||||||
|
|
||||||
(def schema:connectivity
|
(def schema:connectivity
|
||||||
[:map {:title "nitrate-connectivity"}
|
[:map {:title "nitrate-connectivity"}
|
||||||
[:licenses ::sm/boolean]])
|
[:licenses ::sm/boolean]])
|
||||||
|
|
||||||
(sv/defmethod ::get-nitrate-connectivity
|
(sv/defmethod ::get-nitrate-connectivity
|
||||||
{::rpc/auth true
|
{::rpc/auth false
|
||||||
::doc/added "2.14"
|
::doc/added "1.18"
|
||||||
::sm/params [:map]
|
::sm/params [:map]
|
||||||
::sm/result schema:connectivity}
|
::sm/result schema:connectivity}
|
||||||
[cfg _params]
|
[cfg _params]
|
||||||
(nitrate/call cfg :connectivity {}))
|
(nitrate/connectivity cfg))
|
||||||
|
|
||||||
(def ^:private sql:prefix-team-name-and-unset-default
|
|
||||||
"UPDATE team
|
|
||||||
SET name = ? || name,
|
|
||||||
is_default = FALSE
|
|
||||||
WHERE id = ?;")
|
|
||||||
|
|
||||||
(def ^:private sql:get-member-teams-info
|
|
||||||
"SELECT t.id,
|
|
||||||
t.is_default,
|
|
||||||
tpr.is_owner,
|
|
||||||
(SELECT count(*) FROM team_profile_rel WHERE team_id = t.id) AS num_members,
|
|
||||||
(SELECT array_agg(profile_id) FROM team_profile_rel WHERE team_id = t.id) AS member_ids
|
|
||||||
FROM team AS t
|
|
||||||
JOIN team_profile_rel AS tpr ON (tpr.team_id = t.id)
|
|
||||||
WHERE tpr.profile_id = ?
|
|
||||||
AND t.id = ANY(?)
|
|
||||||
AND t.deleted_at IS NULL")
|
|
||||||
|
|
||||||
(def ^:private sql:get-team-files-count
|
|
||||||
"SELECT count(*) AS total
|
|
||||||
FROM file AS f
|
|
||||||
JOIN project AS p ON (p.id = f.project_id)
|
|
||||||
WHERE p.team_id = ?
|
|
||||||
AND f.deleted_at IS NULL")
|
|
||||||
|
|
||||||
(def ^:private schema:leave-org
|
|
||||||
[:map
|
|
||||||
[:id ::sm/uuid]
|
|
||||||
[:name ::sm/text]
|
|
||||||
[:default-team-id ::sm/uuid]
|
|
||||||
[:teams-to-delete
|
|
||||||
[:vector ::sm/uuid]]
|
|
||||||
[:teams-to-leave
|
|
||||||
[:vector
|
|
||||||
[:map
|
|
||||||
[:id ::sm/uuid]
|
|
||||||
[:reassign-to {:optional true} ::sm/uuid]]]]])
|
|
||||||
|
|
||||||
|
|
||||||
(defn- get-organization-teams-for-user
|
|
||||||
[{:keys [::db/conn] :as cfg} org-summary profile-id]
|
|
||||||
(let [org-team-ids (->> (:teams org-summary)
|
|
||||||
(map :id))
|
|
||||||
ids-array (db/create-array conn "uuid" org-team-ids)]
|
|
||||||
(db/exec! conn [sql:get-member-teams-info profile-id ids-array])))
|
|
||||||
|
|
||||||
(defn- calculate-valid-teams
|
|
||||||
([org-teams default-team-id]
|
|
||||||
(let [;; valid default team is the one which id is default-team-id
|
|
||||||
valid-default-team (d/seek #(= default-team-id (:id %)) org-teams)
|
|
||||||
|
|
||||||
;; Remove your-penpot for the rest of validations
|
|
||||||
org-teams (remove #(= default-team-id (:id %)) org-teams)
|
|
||||||
|
|
||||||
;; valid teams to delete are those that the user is owner, and only have one member
|
|
||||||
valid-teams-to-delete-ids (->> org-teams
|
|
||||||
(filter #(and (:is-owner %)
|
|
||||||
(= (:num-members %) 1)))
|
|
||||||
(map :id)
|
|
||||||
(into #{}))
|
|
||||||
;; valid teams to transfer are those that the user is owner, and have more than one member
|
|
||||||
valid-teams-to-transfer (->> org-teams
|
|
||||||
(filter #(and (:is-owner %)
|
|
||||||
(> (:num-members %) 1))))
|
|
||||||
|
|
||||||
;; valid teams to exit are those that the user isn't owner, and have more than one member
|
|
||||||
valid-teams-to-exit (->> org-teams
|
|
||||||
(filter #(and (not (:is-owner %))
|
|
||||||
(> (:num-members %) 1))))]
|
|
||||||
{:valid-teams-to-delete-ids valid-teams-to-delete-ids
|
|
||||||
:valid-teams-to-transfer valid-teams-to-transfer
|
|
||||||
:valid-teams-to-exit valid-teams-to-exit
|
|
||||||
:valid-default-team valid-default-team})))
|
|
||||||
|
|
||||||
(defn get-valid-teams [cfg organization-id profile-id default-team-id]
|
|
||||||
(let [org-summary (nitrate/call cfg :get-org-summary {:organization-id organization-id})
|
|
||||||
org-teams (get-organization-teams-for-user cfg org-summary profile-id)]
|
|
||||||
(calculate-valid-teams org-teams default-team-id)))
|
|
||||||
|
|
||||||
(defn- assert-valid-teams [cfg profile-id organization-id default-team-id teams-to-delete teams-to-leave]
|
|
||||||
(let [org-summary (nitrate/call cfg :get-org-summary {:organization-id organization-id})
|
|
||||||
org-teams (get-organization-teams-for-user cfg org-summary profile-id)
|
|
||||||
{:keys [valid-teams-to-delete-ids
|
|
||||||
valid-teams-to-transfer
|
|
||||||
valid-teams-to-exit
|
|
||||||
valid-default-team]} (calculate-valid-teams org-teams default-team-id)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
valid-teams-to-exit-ids (->> valid-teams-to-exit (map :id) (into #{}))
|
|
||||||
valid-teams-to-transfer-ids (->> valid-teams-to-transfer (map :id) (into #{}))
|
|
||||||
valid-teams-to-leave-ids (into valid-teams-to-transfer-ids valid-teams-to-exit-ids)
|
|
||||||
|
|
||||||
valid-default-team-id? (some? valid-default-team)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
valid-teams-to-delete? (= valid-teams-to-delete-ids (into #{} teams-to-delete))
|
|
||||||
|
|
||||||
;; for every team in teams-to-leave, check that:
|
|
||||||
;; - if it has a reassign-to, it belongs to valid-teams-to-transfer and
|
|
||||||
;; the reassign-to is a member of the team and not the current user;
|
|
||||||
;; - if it hasn't a reassign-to, check that it belongs to valid-teams-to-exit
|
|
||||||
teams-by-id (d/index-by :id org-teams)
|
|
||||||
valid-teams-to-leave? (and
|
|
||||||
(= valid-teams-to-leave-ids (->> teams-to-leave (map :id) (into #{})))
|
|
||||||
(every? (fn [{:keys [id reassign-to]}]
|
|
||||||
(if reassign-to
|
|
||||||
(let [members (db/pgarray->set (:member-ids (get teams-by-id id)))]
|
|
||||||
(and (contains? valid-teams-to-transfer-ids id)
|
|
||||||
(not= reassign-to profile-id)
|
|
||||||
(contains? members reassign-to)))
|
|
||||||
(contains? valid-teams-to-exit-ids id)))
|
|
||||||
teams-to-leave))]
|
|
||||||
;; the org owner cannot leave
|
|
||||||
(when (= (:owner-id org-summary) profile-id)
|
|
||||||
(ex/raise :type :validation
|
|
||||||
:code :org-owner-cannot-leave))
|
|
||||||
|
|
||||||
(when (or
|
|
||||||
(not valid-teams-to-delete?)
|
|
||||||
(not valid-teams-to-leave?)
|
|
||||||
(not valid-default-team-id?))
|
|
||||||
(ex/raise :type :validation
|
|
||||||
:code :not-valid-teams))))
|
|
||||||
|
|
||||||
|
|
||||||
(defn leave-org
|
|
||||||
[{:keys [::db/conn] :as cfg} {:keys [profile-id id name default-team-id teams-to-delete teams-to-leave skip-validation] :as params}]
|
|
||||||
(let [org-prefix (str "[" (d/sanitize-string name) "] ")
|
|
||||||
|
|
||||||
default-team-files-count (-> (db/exec-one! conn [sql:get-team-files-count default-team-id])
|
|
||||||
:total)
|
|
||||||
delete-default-team? (= default-team-files-count 0)]
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
;; assert that the received teams are valid, checking the different constraints
|
|
||||||
(when-not skip-validation
|
|
||||||
(assert-valid-teams cfg profile-id id default-team-id teams-to-delete teams-to-leave))
|
|
||||||
|
|
||||||
(assert-membership cfg profile-id id)
|
|
||||||
|
|
||||||
;; delete the teams-to-delete
|
|
||||||
(doseq [id teams-to-delete]
|
|
||||||
(teams/delete-team cfg {:profile-id profile-id :team-id id}))
|
|
||||||
|
|
||||||
;; leave the teams-to-leave
|
|
||||||
(doseq [{:keys [id reassign-to]} teams-to-leave]
|
|
||||||
(teams/leave-team cfg {:profile-id profile-id :id id :reassign-to reassign-to}))
|
|
||||||
|
|
||||||
;; Delete default-team-id if empty; otherwise keep it and prefix the name.
|
|
||||||
(if delete-default-team?
|
|
||||||
(do
|
|
||||||
(db/update! conn :team {:is-default false} {:id default-team-id})
|
|
||||||
(teams/delete-team cfg {:profile-id profile-id :team-id default-team-id}))
|
|
||||||
(db/exec! conn [sql:prefix-team-name-and-unset-default org-prefix default-team-id]))
|
|
||||||
|
|
||||||
;; Api call to nitrate
|
|
||||||
(nitrate/call cfg :remove-profile-from-org {:profile-id profile-id :organization-id id})
|
|
||||||
|
|
||||||
nil))
|
|
||||||
|
|
||||||
|
|
||||||
(sv/defmethod ::leave-org
|
|
||||||
{::rpc/auth true
|
|
||||||
::doc/added "2.15"
|
|
||||||
::sm/params schema:leave-org
|
|
||||||
::db/transaction true}
|
|
||||||
[cfg {:keys [::rpc/profile-id] :as params}]
|
|
||||||
(leave-org cfg (assoc params :profile-id profile-id)))
|
|
||||||
|
|
||||||
|
|
||||||
(def ^:private schema:remove-team-from-org
|
|
||||||
[:map
|
|
||||||
[:team-id ::sm/uuid]
|
|
||||||
[:organization-id ::sm/uuid]
|
|
||||||
[:organization-name ::sm/text]])
|
|
||||||
|
|
||||||
(sv/defmethod ::remove-team-from-org
|
|
||||||
{::doc/added "2.17"
|
|
||||||
::sm/params schema:remove-team-from-org}
|
|
||||||
[cfg {:keys [::rpc/profile-id team-id organization-id organization-name]}]
|
|
||||||
|
|
||||||
(assert-is-owner cfg profile-id team-id)
|
|
||||||
(assert-not-default-team cfg team-id)
|
|
||||||
(assert-membership cfg profile-id organization-id)
|
|
||||||
|
|
||||||
;; Api call to nitrate
|
|
||||||
(nitrate/call cfg :remove-team-from-org {:team-id team-id :organization-id organization-id})
|
|
||||||
|
|
||||||
;; Notify connected users
|
|
||||||
(notifications/notify-team-change cfg {:id team-id :organization {:name organization-name}} "dashboard.team-no-longer-belong-org")
|
|
||||||
nil)
|
|
||||||
|
|
||||||
|
|
||||||
(def ^:private schema:add-team-to-organization
|
|
||||||
[:map
|
|
||||||
[:team-id ::sm/uuid]
|
|
||||||
[:organization-id ::sm/uuid]])
|
|
||||||
|
|
||||||
(sv/defmethod ::add-team-to-organization
|
|
||||||
{::rpc/auth true
|
|
||||||
::doc/added "2.17"
|
|
||||||
::sm/params schema:add-team-to-organization
|
|
||||||
::db/transaction true}
|
|
||||||
[cfg {:keys [::rpc/profile-id team-id organization-id]}]
|
|
||||||
|
|
||||||
(assert-is-owner cfg profile-id team-id)
|
|
||||||
(assert-not-default-team cfg team-id)
|
|
||||||
(assert-membership cfg profile-id organization-id)
|
|
||||||
|
|
||||||
(let [team-members (db/query cfg :team-profile-rel {:team-id team-id})]
|
|
||||||
;; Add teammates to the org if needed
|
|
||||||
(doseq [{member-id :profile-id} team-members
|
|
||||||
:when (not= member-id profile-id)]
|
|
||||||
(teams/initialize-user-in-nitrate-org cfg member-id organization-id)))
|
|
||||||
|
|
||||||
;; Api call to nitrate
|
|
||||||
(let [team (nitrate/call cfg :set-team-org {:team-id team-id :organization-id organization-id :is-default false})]
|
|
||||||
|
|
||||||
;; Notify connected users
|
|
||||||
(notifications/notify-team-change cfg team "dashboard.team-belong-org"))
|
|
||||||
nil)
|
|
||||||
|
|||||||
@ -314,25 +314,6 @@
|
|||||||
(climit/invoke! generate-thumbnail file))]
|
(climit/invoke! generate-thumbnail file))]
|
||||||
(sto/put-object! storage params)))
|
(sto/put-object! storage params)))
|
||||||
|
|
||||||
;; --- MUTATION: Delete Photo
|
|
||||||
|
|
||||||
(sv/defmethod ::delete-profile-photo
|
|
||||||
{::doc/added "2.17"
|
|
||||||
::sm/params [:map]
|
|
||||||
::sm/result :nil
|
|
||||||
::db/transaction true}
|
|
||||||
[{:keys [::db/conn ::sto/storage]} {:keys [::rpc/profile-id]}]
|
|
||||||
(let [profile (get-profile conn profile-id ::db/for-update true)]
|
|
||||||
(when-let [id (:photo-id profile)]
|
|
||||||
(sto/touch-object! storage id))
|
|
||||||
|
|
||||||
(db/update! conn :profile
|
|
||||||
{:photo-id nil}
|
|
||||||
{:id profile-id}
|
|
||||||
{::db/return-keys false})
|
|
||||||
|
|
||||||
nil))
|
|
||||||
|
|
||||||
;; --- MUTATION: Request Email Change
|
;; --- MUTATION: Request Email Change
|
||||||
|
|
||||||
(declare ^:private request-email-change!)
|
(declare ^:private request-email-change!)
|
||||||
@ -481,9 +462,6 @@
|
|||||||
{:deleted-at deleted-at}
|
{:deleted-at deleted-at}
|
||||||
{:id profile-id})
|
{:id profile-id})
|
||||||
|
|
||||||
;; Api call to nitrate
|
|
||||||
(nitrate/call cfg :remove-profile-from-all-orgs {:profile-id profile-id})
|
|
||||||
|
|
||||||
;; Schedule cascade deletion to a worker
|
;; Schedule cascade deletion to a worker
|
||||||
(wrk/submit! {::db/conn conn
|
(wrk/submit! {::db/conn conn
|
||||||
::wrk/task :delete-object
|
::wrk/task :delete-object
|
||||||
|
|||||||
@ -471,8 +471,8 @@
|
|||||||
;; --- COMMAND QUERY: get-team-info
|
;; --- COMMAND QUERY: get-team-info
|
||||||
|
|
||||||
(defn get-team-info
|
(defn get-team-info
|
||||||
[cfg {:keys [id] :as params}]
|
[{:keys [::db/conn] :as cfg} {:keys [id] :as params}]
|
||||||
(-> (db/get* cfg :team
|
(-> (db/get* conn :team
|
||||||
{:id id}
|
{:id id}
|
||||||
{::sql/columns [:id :is-default :features]})
|
{::sql/columns [:id :is-default :features]})
|
||||||
(decode-row)))
|
(decode-row)))
|
||||||
@ -499,9 +499,7 @@
|
|||||||
[:map {:title "create-team"}
|
[:map {:title "create-team"}
|
||||||
[:name [:string {:max 250}]]
|
[:name [:string {:max 250}]]
|
||||||
[:features {:optional true} ::cfeat/features]
|
[:features {:optional true} ::cfeat/features]
|
||||||
[:id {:optional true} ::sm/uuid]
|
[:id {:optional true} ::sm/uuid]])
|
||||||
[:organization-id {:optional true} ::sm/uuid]
|
|
||||||
[:is-default {:optional true} :boolean]])
|
|
||||||
|
|
||||||
(sv/defmethod ::create-team
|
(sv/defmethod ::create-team
|
||||||
{::doc/added "1.17"
|
{::doc/added "1.17"
|
||||||
@ -522,89 +520,17 @@
|
|||||||
(with-meta team
|
(with-meta team
|
||||||
{::audit/props {:id (:id team)}})))
|
{::audit/props {:id (:id team)}})))
|
||||||
|
|
||||||
|
|
||||||
(defn create-default-org-team
|
|
||||||
[cfg profile-id organization-id]
|
|
||||||
(quotes/check! cfg {::quotes/id ::quotes/teams-per-profile
|
|
||||||
::quotes/profile-id profile-id})
|
|
||||||
|
|
||||||
(let [features (-> (cfeat/get-enabled-features cf/flags)
|
|
||||||
(set/difference cfeat/frontend-only-features)
|
|
||||||
(set/difference cfeat/no-team-inheritable-features))
|
|
||||||
params {:profile-id profile-id
|
|
||||||
:name "Your Penpot"
|
|
||||||
:features features
|
|
||||||
:organization-id organization-id
|
|
||||||
:is-default true}
|
|
||||||
team (create-team cfg params)]
|
|
||||||
(select-keys team [:id])))
|
|
||||||
|
|
||||||
(defn initialize-user-in-nitrate-org
|
|
||||||
"If needed, create a default team for the user on the organization,
|
|
||||||
and notify Nitrate that an user has been added to an org."
|
|
||||||
([cfg profile-id organization-id]
|
|
||||||
(initialize-user-in-nitrate-org cfg profile-id organization-id nil))
|
|
||||||
([cfg profile-id organization-id email]
|
|
||||||
(assert (db/connection-map? cfg)
|
|
||||||
"expected cfg with valid connection")
|
|
||||||
(when (contains? cf/flags :nitrate)
|
|
||||||
(db/tx-run!
|
|
||||||
cfg
|
|
||||||
(fn [{:keys [::db/conn] :as tx-cfg}]
|
|
||||||
|
|
||||||
(let [membership (nitrate/call cfg :get-org-membership {:profile-id profile-id
|
|
||||||
:organization-id organization-id})]
|
|
||||||
;; Only when the user doesn't belong to the organization yet
|
|
||||||
(when (and
|
|
||||||
(some? (:organization-id membership)) ;; the organization exists
|
|
||||||
(not (:is-member membership))) ;; the user is not a member of the org yet
|
|
||||||
|
|
||||||
|
|
||||||
(let [organization-id organization-id
|
|
||||||
default-team (create-default-org-team (assoc tx-cfg ::db/conn conn) profile-id organization-id)
|
|
||||||
default-team-id (:id default-team)
|
|
||||||
result (nitrate/call tx-cfg :add-profile-to-org (cond-> {:profile-id profile-id
|
|
||||||
:team-id default-team-id
|
|
||||||
:organization-id organization-id}
|
|
||||||
(some? email) (assoc :email email)))]
|
|
||||||
(when (not (:is-member result))
|
|
||||||
(ex/raise :type :internal
|
|
||||||
:code :failed-add-profile-org-nitrate
|
|
||||||
:context {:profile-id profile-id
|
|
||||||
:organization-id organization-id
|
|
||||||
:default-team-id default-team-id}))
|
|
||||||
default-team-id))))))))
|
|
||||||
|
|
||||||
(defn add-profile-to-team!
|
|
||||||
([cfg params]
|
|
||||||
(add-profile-to-team! cfg params nil))
|
|
||||||
([{:keys [::db/conn] :as cfg} {:keys [:profile-id :team-id] :as params} options]
|
|
||||||
(assert (db/connection-map? cfg)
|
|
||||||
"expected cfg with valid connection")
|
|
||||||
(when (contains? cf/flags :nitrate)
|
|
||||||
(let [membership (nitrate/call cfg :get-org-membership-by-team {:profile-id profile-id :team-id team-id})]
|
|
||||||
;; Only when the team belong to an organization and the user is not a member
|
|
||||||
(when (and
|
|
||||||
(some? (:organization-id membership)) ;; the team do belong to an organization
|
|
||||||
(not (:is-member membership))) ;; the user is not a member of the org yet
|
|
||||||
(initialize-user-in-nitrate-org cfg profile-id (:organization-id membership)))))
|
|
||||||
(db/insert! conn :team-profile-rel params options)))
|
|
||||||
|
|
||||||
(defn create-team
|
(defn create-team
|
||||||
"This is a complete team creation process, it creates the team
|
"This is a complete team creation process, it creates the team
|
||||||
object and all related objects (default role and default project)."
|
object and all related objects (default role and default project)."
|
||||||
[{:keys [::db/conn] :as cfg} params]
|
[cfg-or-conn params]
|
||||||
(assert (db/connection-map? cfg)
|
(let [conn (db/get-connection cfg-or-conn)
|
||||||
"expected cfg with valid connection")
|
team (create-team* conn params)
|
||||||
(let [team (create-team* conn params)
|
|
||||||
params (assoc params
|
params (assoc params
|
||||||
:team-id (:id team)
|
:team-id (:id team)
|
||||||
:role :owner)
|
:role :owner)
|
||||||
project (create-team-default-project conn params)]
|
project (create-team-default-project conn params)]
|
||||||
(create-team-role cfg params)
|
(create-team-role conn params)
|
||||||
;; Set team organization in Nitrate if organization-id is provided
|
|
||||||
(when (and (contains? cf/flags :nitrate) (:organization-id params))
|
|
||||||
(nitrate/set-team-organization cfg team params))
|
|
||||||
(assoc team :default-project-id (:id project))))
|
(assoc team :default-project-id (:id project))))
|
||||||
|
|
||||||
(defn- create-team*
|
(defn- create-team*
|
||||||
@ -620,13 +546,11 @@
|
|||||||
(decode-row team)))
|
(decode-row team)))
|
||||||
|
|
||||||
(defn- create-team-role
|
(defn- create-team-role
|
||||||
[cfg {:keys [profile-id team-id role] :as params}]
|
[conn {:keys [profile-id team-id role] :as params}]
|
||||||
(assert (db/connection-map? cfg)
|
|
||||||
"expected cfg with valid connection")
|
|
||||||
(let [params {:team-id team-id
|
(let [params {:team-id team-id
|
||||||
:profile-id profile-id}]
|
:profile-id profile-id}]
|
||||||
(->> (perms/assign-role-flags params role)
|
(->> (perms/assign-role-flags params role)
|
||||||
(add-profile-to-team! cfg))))
|
(db/insert! conn :team-profile-rel))))
|
||||||
|
|
||||||
(defn- create-team-default-project
|
(defn- create-team-default-project
|
||||||
[conn {:keys [profile-id team-id] :as params}]
|
[conn {:keys [profile-id team-id] :as params}]
|
||||||
@ -685,7 +609,7 @@
|
|||||||
;; --- Mutation: Leave Team
|
;; --- Mutation: Leave Team
|
||||||
|
|
||||||
(defn leave-team
|
(defn leave-team
|
||||||
[{:keys [::db/conn ::mbus/msgbus]} {:keys [profile-id id reassign-to]}]
|
[conn {:keys [profile-id id reassign-to]}]
|
||||||
(let [perms (get-permissions conn profile-id id)
|
(let [perms (get-permissions conn profile-id id)
|
||||||
members (get-team-members conn id)]
|
members (get-team-members conn id)]
|
||||||
|
|
||||||
@ -700,9 +624,7 @@
|
|||||||
;; if the `reassign-to` is filled and has a different value
|
;; if the `reassign-to` is filled and has a different value
|
||||||
;; than the current profile-id, we proceed to reassing the
|
;; than the current profile-id, we proceed to reassing the
|
||||||
;; owner role to profile identified by the `reassign-to`.
|
;; owner role to profile identified by the `reassign-to`.
|
||||||
;; Ignore the reasignation if the current profile is not
|
(and reassign-to (not= reassign-to profile-id))
|
||||||
;; the owner
|
|
||||||
(and reassign-to (not= reassign-to profile-id) (:is-owner perms))
|
|
||||||
(let [member (d/seek #(= reassign-to (:id %)) members)]
|
(let [member (d/seek #(= reassign-to (:id %)) members)]
|
||||||
(when-not member
|
(when-not member
|
||||||
(ex/raise :type :not-found :code :member-does-not-exist))
|
(ex/raise :type :not-found :code :member-does-not-exist))
|
||||||
@ -716,15 +638,7 @@
|
|||||||
;; assign owner role to new profile
|
;; assign owner role to new profile
|
||||||
(db/update! conn :team-profile-rel
|
(db/update! conn :team-profile-rel
|
||||||
(get types.team/permissions-for-role :owner)
|
(get types.team/permissions-for-role :owner)
|
||||||
{:team-id id :profile-id reassign-to})
|
{:team-id id :profile-id reassign-to}))
|
||||||
|
|
||||||
;; notify new owner
|
|
||||||
(mbus/pub! msgbus
|
|
||||||
:topic reassign-to
|
|
||||||
:message {:type :team-role-change
|
|
||||||
:topic reassign-to
|
|
||||||
:team-id id
|
|
||||||
:role :owner}))
|
|
||||||
|
|
||||||
;; and finally, if all other conditions does not match and the
|
;; and finally, if all other conditions does not match and the
|
||||||
;; current profile is owner, we dont allow it because there
|
;; current profile is owner, we dont allow it because there
|
||||||
@ -749,44 +663,32 @@
|
|||||||
{::doc/added "1.17"
|
{::doc/added "1.17"
|
||||||
::sm/params schema:leave-team
|
::sm/params schema:leave-team
|
||||||
::db/transaction true}
|
::db/transaction true}
|
||||||
[cfg {:keys [::rpc/profile-id] :as params}]
|
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id] :as params}]
|
||||||
(leave-team cfg (assoc params :profile-id profile-id)))
|
(leave-team conn (assoc params :profile-id profile-id)))
|
||||||
|
|
||||||
|
|
||||||
;; --- Mutation: Delete Team
|
;; --- Mutation: Delete Team
|
||||||
|
|
||||||
(defn delete-team
|
(defn- delete-team
|
||||||
"Mark a team for deletion"
|
"Mark a team for deletion"
|
||||||
[{:keys [::db/conn] :as cfg} {:keys [profile-id team-id]}]
|
[conn {:keys [id] :as team}]
|
||||||
|
|
||||||
(let [team (get-team conn :profile-id profile-id :team-id team-id)
|
(let [delay (ldel/get-deletion-delay team)
|
||||||
perms (get team :permissions)]
|
team (db/update! conn :team
|
||||||
|
{:deleted-at (ct/in-future delay)}
|
||||||
(when-not (:is-owner perms)
|
{:id id}
|
||||||
(ex/raise :type :validation
|
{::db/return-keys true})]
|
||||||
:code :only-owner-can-delete-team))
|
|
||||||
|
|
||||||
(when (:is-default team)
|
(when (:is-default team)
|
||||||
(ex/raise :type :validation
|
(ex/raise :type :validation
|
||||||
:code :non-deletable-team
|
:code :non-deletable-team
|
||||||
:hint "impossible to delete default team"))
|
:hint "impossible to delete default team"))
|
||||||
|
|
||||||
(let [delay (ldel/get-deletion-delay team)
|
(wrk/submit! {::db/conn conn
|
||||||
team (db/update! conn :team
|
::wrk/task :delete-object
|
||||||
{:deleted-at (ct/in-future delay)}
|
::wrk/params {:object :team
|
||||||
{:id team-id}
|
:deleted-at (:deleted-at team)
|
||||||
{::db/return-keys true})]
|
:id id}})
|
||||||
|
team))
|
||||||
;; Api call to nitrate
|
|
||||||
(when (contains? cf/flags :nitrate)
|
|
||||||
(nitrate/call cfg :delete-team {:profile-id profile-id :team-id team-id}))
|
|
||||||
|
|
||||||
(wrk/submit! {::db/conn conn
|
|
||||||
::wrk/task :delete-object
|
|
||||||
::wrk/params {:object :team
|
|
||||||
:deleted-at (:deleted-at team)
|
|
||||||
:id team-id}})
|
|
||||||
team)))
|
|
||||||
|
|
||||||
(def ^:private schema:delete-team
|
(def ^:private schema:delete-team
|
||||||
[:map {:title "delete-team"}
|
[:map {:title "delete-team"}
|
||||||
@ -796,9 +698,16 @@
|
|||||||
{::doc/added "1.17"
|
{::doc/added "1.17"
|
||||||
::sm/params schema:delete-team
|
::sm/params schema:delete-team
|
||||||
::db/transaction true}
|
::db/transaction true}
|
||||||
[cfg {:keys [::rpc/profile-id id] :as params}]
|
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id id] :as params}]
|
||||||
(delete-team cfg {:team-id id :profile-id profile-id})
|
(let [team (get-team conn :profile-id profile-id :team-id id)
|
||||||
nil)
|
perms (get team :permissions)]
|
||||||
|
|
||||||
|
(when-not (:is-owner perms)
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code :only-owner-can-delete-team))
|
||||||
|
|
||||||
|
(delete-team conn team)
|
||||||
|
nil))
|
||||||
|
|
||||||
;; --- Mutation: Team Update Role
|
;; --- Mutation: Team Update Role
|
||||||
|
|
||||||
|
|||||||
@ -21,7 +21,6 @@
|
|||||||
[app.email :as eml]
|
[app.email :as eml]
|
||||||
[app.loggers.audit :as audit]
|
[app.loggers.audit :as audit]
|
||||||
[app.main :as-alias main]
|
[app.main :as-alias main]
|
||||||
[app.nitrate :as nitrate]
|
|
||||||
[app.rpc :as-alias rpc]
|
[app.rpc :as-alias rpc]
|
||||||
[app.rpc.commands.profile :as profile]
|
[app.rpc.commands.profile :as profile]
|
||||||
[app.rpc.commands.teams :as teams]
|
[app.rpc.commands.teams :as teams]
|
||||||
@ -36,29 +35,20 @@
|
|||||||
;; --- Mutation: Create Team Invitation
|
;; --- Mutation: Create Team Invitation
|
||||||
|
|
||||||
(def sql:upsert-team-invitation
|
(def sql:upsert-team-invitation
|
||||||
"insert into team_invitation(id, team_id, org_id, email_to, created_by, role, valid_until)
|
"insert into team_invitation(id, team_id, email_to, created_by, role, valid_until)
|
||||||
values (?, ?, null, ?, ?, ?, ?)
|
values (?, ?, ?, ?, ?, ?)
|
||||||
on conflict(team_id, email_to) do
|
on conflict(team_id, email_to) do
|
||||||
update set role = ?, valid_until = ?, updated_at = now()
|
update set role = ?, valid_until = ?, updated_at = now()
|
||||||
returning *")
|
returning *")
|
||||||
|
|
||||||
(def sql:upsert-org-invitation
|
|
||||||
"insert into team_invitation(id, team_id, org_id, email_to, created_by, role, valid_until)
|
|
||||||
values (?, null, ?, ?, ?, ?, ?)
|
|
||||||
on conflict(org_id, email_to) where team_id is null do
|
|
||||||
update set role = ?, valid_until = ?, updated_at = now()
|
|
||||||
returning *")
|
|
||||||
|
|
||||||
(defn- create-invitation-token
|
(defn- create-invitation-token
|
||||||
[cfg {:keys [profile-id valid-until organization-id organization-name team-id member-id member-email role]}]
|
[cfg {:keys [profile-id valid-until team-id member-id member-email role]}]
|
||||||
(tokens/generate cfg
|
(tokens/generate cfg
|
||||||
{:iss :team-invitation
|
{:iss :team-invitation
|
||||||
:exp valid-until
|
:exp valid-until
|
||||||
:profile-id profile-id
|
:profile-id profile-id
|
||||||
:role role
|
:role role
|
||||||
:team-id team-id
|
:team-id team-id
|
||||||
:organization-id organization-id
|
|
||||||
:organization-name organization-name
|
|
||||||
:member-email member-email
|
:member-email member-email
|
||||||
:member-id member-id}))
|
:member-id member-id}))
|
||||||
|
|
||||||
@ -84,40 +74,19 @@
|
|||||||
[:role types.team/schema:role]
|
[:role types.team/schema:role]
|
||||||
[:email ::sm/email]])
|
[:email ::sm/email]])
|
||||||
|
|
||||||
(def ^:private schema:create-org-invitation
|
|
||||||
[:map {:title "params:create-org-invitation"}
|
|
||||||
[::rpc/profile-id ::sm/uuid]
|
|
||||||
[:organization
|
|
||||||
[:map
|
|
||||||
[:id ::sm/uuid]
|
|
||||||
[:name :string]
|
|
||||||
[:logo ::sm/uri]]]
|
|
||||||
[:profile
|
|
||||||
[:map
|
|
||||||
[:id ::sm/uuid]
|
|
||||||
[:fullname :string]]]
|
|
||||||
[:role types.team/schema:role]
|
|
||||||
[:email ::sm/email]])
|
|
||||||
|
|
||||||
(def ^:private check-create-invitation-params
|
(def ^:private check-create-invitation-params
|
||||||
(sm/check-fn schema:create-invitation))
|
(sm/check-fn schema:create-invitation))
|
||||||
|
|
||||||
(def ^:private check-create-org-invitation-params
|
|
||||||
(sm/check-fn schema:create-org-invitation))
|
|
||||||
|
|
||||||
(defn- allow-invitation-emails?
|
(defn- allow-invitation-emails?
|
||||||
[member]
|
[member]
|
||||||
(let [notifications (dm/get-in member [:props :notifications])]
|
(let [notifications (dm/get-in member [:props :notifications])]
|
||||||
(not= :none (:email-invites notifications))))
|
(not= :none (:email-invites notifications))))
|
||||||
|
|
||||||
(defn- create-invitation
|
(defn- create-invitation
|
||||||
[{:keys [::db/conn] :as cfg} {:keys [team organization profile role email] :as params}]
|
[{:keys [::db/conn] :as cfg} {:keys [team profile role email] :as params}]
|
||||||
|
|
||||||
(assert (db/connection-map? cfg)
|
(assert (db/connection? conn) "expected valid connection on cfg parameter")
|
||||||
"expected cfg with valid connection")
|
(assert (check-create-invitation-params params))
|
||||||
(if organization
|
|
||||||
(assert (check-create-org-invitation-params params))
|
|
||||||
(assert (check-create-invitation-params params)))
|
|
||||||
|
|
||||||
(let [email (profile/clean-email email)
|
(let [email (profile/clean-email email)
|
||||||
member (profile/get-profile-by-email conn email)]
|
member (profile/get-profile-by-email conn email)]
|
||||||
@ -134,12 +103,9 @@
|
|||||||
:profile-id (:id member)}
|
:profile-id (:id member)}
|
||||||
(get types.team/permissions-for-role role))]
|
(get types.team/permissions-for-role role))]
|
||||||
|
|
||||||
(if organization
|
;; Insert the invited member to the team
|
||||||
;; Insert the invited member to the org
|
(db/insert! conn :team-profile-rel params
|
||||||
(when (contains? cf/flags :nitrate)
|
{::db/on-conflict-do-nothing? true})
|
||||||
(teams/initialize-user-in-nitrate-org cfg (:id member) (:id organization) email))
|
|
||||||
;; Insert the invited member to the team
|
|
||||||
(teams/add-profile-to-team! cfg params {::db/on-conflict-do-nothing? true}))
|
|
||||||
|
|
||||||
;; If profile is not yet verified, mark it as verified because
|
;; If profile is not yet verified, mark it as verified because
|
||||||
;; accepting an invitation link serves as verification.
|
;; accepting an invitation link serves as verification.
|
||||||
@ -156,30 +122,18 @@
|
|||||||
(teams/check-email-spam conn email true)
|
(teams/check-email-spam conn email true)
|
||||||
|
|
||||||
(let [id (uuid/next)
|
(let [id (uuid/next)
|
||||||
expire (if organization
|
expire (ct/in-future "168h") ;; 7 days
|
||||||
(ct/in-future "876000h") ;; Organization invitations doesn't expire
|
invitation (db/exec-one! conn [sql:upsert-team-invitation id
|
||||||
(ct/in-future "168h")) ;; 7 days
|
(:id team) (str/lower email)
|
||||||
invitation (db/exec-one! conn (if organization
|
(:id profile)
|
||||||
[sql:upsert-org-invitation id
|
(name role) expire
|
||||||
(:id organization)
|
(name role) expire])
|
||||||
(str/lower email)
|
|
||||||
(:id profile)
|
|
||||||
(name role) expire
|
|
||||||
(name role) expire]
|
|
||||||
[sql:upsert-team-invitation id
|
|
||||||
(:id team)
|
|
||||||
(str/lower email)
|
|
||||||
(:id profile)
|
|
||||||
(name role) expire
|
|
||||||
(name role) expire]))
|
|
||||||
updated? (not= id (:id invitation))
|
updated? (not= id (:id invitation))
|
||||||
profile-id (:id profile)
|
profile-id (:id profile)
|
||||||
tprops {:profile-id profile-id
|
tprops {:profile-id profile-id
|
||||||
:invitation-id (:id invitation)
|
:invitation-id (:id invitation)
|
||||||
:valid-until expire
|
:valid-until expire
|
||||||
:team-id (:id team)
|
:team-id (:id team)
|
||||||
:organization-id (:id organization)
|
|
||||||
:organization-name (:name organization)
|
|
||||||
:member-email (:email-to invitation)
|
:member-email (:email-to invitation)
|
||||||
:member-id (:id member)
|
:member-id (:id member)
|
||||||
:role role}
|
:role role}
|
||||||
@ -191,58 +145,28 @@
|
|||||||
|
|
||||||
(let [props (-> (dissoc tprops :profile-id)
|
(let [props (-> (dissoc tprops :profile-id)
|
||||||
(audit/clean-props))
|
(audit/clean-props))
|
||||||
evname (cond
|
evname (if updated?
|
||||||
(and updated? organization) "update-org-invitation"
|
"update-team-invitation"
|
||||||
updated? "update-team-invitation"
|
"create-team-invitation")
|
||||||
organization "create-org-invitation"
|
|
||||||
:else "create-team-invitation")
|
|
||||||
event (-> (audit/event-from-rpc-params params)
|
event (-> (audit/event-from-rpc-params params)
|
||||||
(assoc ::audit/name evname)
|
(assoc ::audit/name evname)
|
||||||
(assoc ::audit/props props))]
|
(assoc ::audit/props props))]
|
||||||
(audit/submit! cfg event))
|
(audit/submit! cfg event))
|
||||||
|
|
||||||
(when (allow-invitation-emails? member)
|
(when (allow-invitation-emails? member)
|
||||||
(if organization
|
(eml/send! {::eml/conn conn
|
||||||
(when (contains? cf/flags :nitrate)
|
::eml/factory eml/invite-to-team
|
||||||
(eml/send! {::eml/conn conn
|
:public-uri (cf/get :public-uri)
|
||||||
::eml/factory eml/invite-to-org
|
:to email
|
||||||
:public-uri (cf/get :public-uri)
|
:invited-by (:fullname profile)
|
||||||
:to email
|
:team (:name team)
|
||||||
:invited-by (:fullname profile)
|
:token itoken
|
||||||
:user-name (:fullname member)
|
:extra-data ptoken}))
|
||||||
:organization-name (:name organization)
|
|
||||||
:org-logo (:logo organization)
|
|
||||||
:org-initials (d/get-initials (:name organization))
|
|
||||||
:token itoken
|
|
||||||
:extra-data ptoken}))
|
|
||||||
(let [team (if (contains? cf/flags :nitrate)
|
|
||||||
(nitrate/add-org-info-to-team cfg team {})
|
|
||||||
team)]
|
|
||||||
(eml/send! {::eml/conn conn
|
|
||||||
::eml/factory eml/invite-to-team
|
|
||||||
:public-uri (cf/get :public-uri)
|
|
||||||
:to email
|
|
||||||
:invited-by (:fullname profile)
|
|
||||||
:team (:name team)
|
|
||||||
:organization (:organization-name team)
|
|
||||||
:token itoken
|
|
||||||
:extra-data ptoken}))))
|
|
||||||
|
|
||||||
itoken)))))
|
itoken)))))
|
||||||
|
|
||||||
(defn create-org-invitation
|
|
||||||
[cfg {:keys [::rpc/profile-id id name logo] :as params}]
|
|
||||||
(let [profile (db/get-by-id cfg :profile profile-id)]
|
|
||||||
(create-invitation cfg
|
|
||||||
(assoc params
|
|
||||||
:organization {:id id :name name :logo logo}
|
|
||||||
:profile profile
|
|
||||||
:role :editor))))
|
|
||||||
|
|
||||||
(defn- add-member-to-team
|
(defn- add-member-to-team
|
||||||
[{:keys [::db/conn] :as cfg} profile team role member]
|
[conn profile team role member]
|
||||||
(assert (db/connection-map? cfg)
|
|
||||||
"expected cfg with valid connection")
|
|
||||||
|
|
||||||
(let [team-id (:id team)
|
(let [team-id (:id team)
|
||||||
params (merge
|
params (merge
|
||||||
@ -262,7 +186,7 @@
|
|||||||
::quotes/team-id team-id})
|
::quotes/team-id team-id})
|
||||||
|
|
||||||
;; Insert the member to the team
|
;; Insert the member to the team
|
||||||
(teams/add-profile-to-team! cfg params {::db/on-conflict-do-nothing? true})
|
(db/insert! conn :team-profile-rel params {::db/on-conflict-do-nothing? true})
|
||||||
|
|
||||||
;; Delete any request
|
;; Delete any request
|
||||||
(db/delete! conn :team-access-request
|
(db/delete! conn :team-access-request
|
||||||
@ -344,7 +268,7 @@
|
|||||||
(filter #(contains? invitation-emails (key %)))
|
(filter #(contains? invitation-emails (key %)))
|
||||||
(map (fn [[email member]]
|
(map (fn [[email member]]
|
||||||
(let [role (:role (first (filter #(= (:email %) email) invitation-data)))]
|
(let [role (:role (first (filter #(= (:email %) email) invitation-data)))]
|
||||||
(add-member-to-team cfg profile team role member))))
|
(add-member-to-team conn profile team role member))))
|
||||||
(doall))
|
(doall))
|
||||||
|
|
||||||
invitations))
|
invitations))
|
||||||
|
|||||||
@ -16,10 +16,8 @@
|
|||||||
[app.http.session :as session]
|
[app.http.session :as session]
|
||||||
[app.loggers.audit :as audit]
|
[app.loggers.audit :as audit]
|
||||||
[app.main :as-alias main]
|
[app.main :as-alias main]
|
||||||
[app.nitrate :as nitrate]
|
|
||||||
[app.rpc :as-alias rpc]
|
[app.rpc :as-alias rpc]
|
||||||
[app.rpc.commands.profile :as profile]
|
[app.rpc.commands.profile :as profile]
|
||||||
[app.rpc.commands.teams :as teams]
|
|
||||||
[app.rpc.doc :as-alias doc]
|
[app.rpc.doc :as-alias doc]
|
||||||
[app.rpc.helpers :as rph]
|
[app.rpc.helpers :as rph]
|
||||||
[app.rpc.quotes :as quotes]
|
[app.rpc.quotes :as quotes]
|
||||||
@ -88,74 +86,52 @@
|
|||||||
;; --- Team Invitation
|
;; --- Team Invitation
|
||||||
|
|
||||||
(defn- accept-invitation
|
(defn- accept-invitation
|
||||||
[{:keys [::db/conn] :as cfg}
|
[{:keys [::db/conn] :as cfg} {:keys [team-id role member-email] :as claims} invitation member]
|
||||||
{:keys [team-id organization-id role member-email] :as claims} invitation member]
|
|
||||||
(let [;; Update the role if there is an invitation
|
(let [;; Update the role if there is an invitation
|
||||||
role (or (some-> invitation :role keyword) role)
|
role (or (some-> invitation :role keyword) role)
|
||||||
id-member (:id member)]
|
params (merge
|
||||||
|
{:team-id team-id
|
||||||
|
:profile-id (:id member)}
|
||||||
|
(get types.team/permissions-for-role role))]
|
||||||
|
|
||||||
;; Do not allow blocked users accept invitations.
|
;; Do not allow blocked users accept invitations.
|
||||||
(when (:is-blocked member)
|
(when (:is-blocked member)
|
||||||
(ex/raise :type :restriction
|
(ex/raise :type :restriction
|
||||||
:code :profile-blocked))
|
:code :profile-blocked))
|
||||||
|
|
||||||
(when team-id
|
(quotes/check! cfg {::quotes/id ::quotes/profiles-per-team
|
||||||
(quotes/check! cfg {::quotes/id ::quotes/profiles-per-team
|
::quotes/profile-id (:id member)
|
||||||
::quotes/profile-id id-member
|
::quotes/team-id team-id})
|
||||||
::quotes/team-id team-id}))
|
|
||||||
|
|
||||||
(let [params (merge
|
;; Insert the invited member to the team
|
||||||
{:team-id team-id
|
(db/insert! conn :team-profile-rel params {::db/on-conflict-do-nothing? true})
|
||||||
:profile-id id-member}
|
|
||||||
(get types.team/permissions-for-role role))
|
|
||||||
|
|
||||||
accepted-team-id (if organization-id
|
;; If profile is not yet verified, mark it as verified because
|
||||||
;; Insert the invited member to the org
|
;; accepting an invitation link serves as verification.
|
||||||
(when (contains? cf/flags :nitrate)
|
(when-not (:is-active member)
|
||||||
(teams/initialize-user-in-nitrate-org cfg id-member organization-id member-email))
|
(db/update! conn :profile
|
||||||
;; Insert the invited member to the team
|
{:is-active true}
|
||||||
(do (teams/add-profile-to-team! cfg params {::db/on-conflict-do-nothing? true})
|
{:id (:id member)}))
|
||||||
team-id))]
|
|
||||||
|
|
||||||
(when-not accepted-team-id
|
;; Delete the invitation
|
||||||
(ex/raise :type :internal
|
(db/delete! conn :team-invitation
|
||||||
:code :accept-invitation-failed
|
{:team-id team-id :email-to member-email})
|
||||||
:hint "the accept invitation has failed"))
|
|
||||||
|
|
||||||
|
;; Delete any request
|
||||||
|
(db/delete! conn :team-access-request
|
||||||
|
{:team-id team-id :requester-id (:id member)})
|
||||||
|
|
||||||
;; If profile is not yet verified, mark it as verified because
|
(assoc member :is-active true)))
|
||||||
;; accepting an invitation link serves as verification.
|
|
||||||
(when-not (:is-active member)
|
|
||||||
(db/update! conn :profile
|
|
||||||
{:is-active true}
|
|
||||||
{:id id-member}))
|
|
||||||
|
|
||||||
;; Delete the invitation
|
|
||||||
(db/delete! conn :team-invitation
|
|
||||||
(cond-> {:email-to member-email}
|
|
||||||
team-id (assoc :team-id team-id)
|
|
||||||
organization-id (assoc :org-id organization-id)))
|
|
||||||
|
|
||||||
;; Delete any request (only applicable for team invitations)
|
|
||||||
(when team-id
|
|
||||||
(db/delete! conn :team-access-request
|
|
||||||
{:team-id team-id :requester-id id-member}))
|
|
||||||
|
|
||||||
accepted-team-id)))
|
|
||||||
|
|
||||||
(def schema:team-invitation-claims
|
(def schema:team-invitation-claims
|
||||||
[:and
|
[:map {:title "TeamInvitationClaims"}
|
||||||
[:map {:title "TeamInvitationClaims"}
|
[:iss :keyword]
|
||||||
[:iss :keyword]
|
[:exp ::ct/inst]
|
||||||
[:exp ::ct/inst]
|
[:profile-id ::sm/uuid]
|
||||||
[:profile-id ::sm/uuid]
|
[:role types.team/schema:role]
|
||||||
[:role types.team/schema:role]
|
[:team-id ::sm/uuid]
|
||||||
[:team-id {:optional true} ::sm/uuid]
|
[:member-email ::sm/email]
|
||||||
[:organization-id {:optional true} ::sm/uuid]
|
[:member-id {:optional true} ::sm/uuid]])
|
||||||
[:member-email ::sm/email]
|
|
||||||
[:member-id {:optional true} ::sm/uuid]]
|
|
||||||
[:fn {:error/message "team-id or organization-id must be present"}
|
|
||||||
(fn [m] (or (:team-id m) (:organization-id m)))]])
|
|
||||||
|
|
||||||
(def valid-team-invitation-claims?
|
(def valid-team-invitation-claims?
|
||||||
(sm/lazy-validator schema:team-invitation-claims))
|
(sm/lazy-validator schema:team-invitation-claims))
|
||||||
@ -163,7 +139,7 @@
|
|||||||
(defmethod process-token :team-invitation
|
(defmethod process-token :team-invitation
|
||||||
[{:keys [::db/conn] :as cfg}
|
[{:keys [::db/conn] :as cfg}
|
||||||
{:keys [::rpc/profile-id token] :as params}
|
{:keys [::rpc/profile-id token] :as params}
|
||||||
{:keys [member-id team-id organization-id member-email] :as claims}]
|
{:keys [member-id team-id member-email] :as claims}]
|
||||||
|
|
||||||
(when-not (valid-team-invitation-claims? claims)
|
(when-not (valid-team-invitation-claims? claims)
|
||||||
(ex/raise :type :validation
|
(ex/raise :type :validation
|
||||||
@ -171,44 +147,19 @@
|
|||||||
:hint "invitation token contains unexpected data"))
|
:hint "invitation token contains unexpected data"))
|
||||||
|
|
||||||
(let [invitation (db/get* conn :team-invitation
|
(let [invitation (db/get* conn :team-invitation
|
||||||
(cond-> {:email-to member-email}
|
{:team-id team-id :email-to member-email})
|
||||||
team-id (assoc :team-id team-id)
|
|
||||||
organization-id (assoc :org-id organization-id)))
|
|
||||||
profile (db/get* conn :profile
|
profile (db/get* conn :profile
|
||||||
{:id profile-id}
|
{:id profile-id}
|
||||||
{:columns [:id :email :default-team-id]})
|
{:columns [:id :email]})
|
||||||
registration-disabled? (not (contains? cf/flags :registration))
|
registration-disabled? (not (contains? cf/flags :registration))]
|
||||||
|
(when (nil? invitation)
|
||||||
org-invitation? (and (contains? cf/flags :nitrate) organization-id)
|
(ex/raise :type :validation
|
||||||
membership (when org-invitation?
|
:code :invalid-token
|
||||||
(nitrate/call cfg :get-org-membership {:profile-id profile-id
|
:hint "no invitation associated with the token"))
|
||||||
:organization-id organization-id}))]
|
|
||||||
|
|
||||||
(if profile
|
|
||||||
(do
|
|
||||||
(when-not (or (= member-id profile-id)
|
|
||||||
(= member-email (:email profile)))
|
|
||||||
(ex/raise :type :validation
|
|
||||||
:code :invalid-token
|
|
||||||
:hint "logged-in user does not matches the invitation"))
|
|
||||||
|
|
||||||
(when (:is-member membership)
|
|
||||||
(ex/raise :type :validation
|
|
||||||
:code :already-an-org-member
|
|
||||||
:team-id (:default-team-id membership)
|
|
||||||
:hint "the user is already a member of the organization"))
|
|
||||||
|
|
||||||
(when (and org-invitation? (not (:organization-id membership)))
|
|
||||||
(ex/raise :type :validation
|
|
||||||
:code :org-not-found
|
|
||||||
:team-id (:default-team-id profile)
|
|
||||||
:hint "the organization doesn't exist"))
|
|
||||||
|
|
||||||
(when (nil? invitation)
|
|
||||||
(ex/raise :type :validation
|
|
||||||
:code :invalid-token
|
|
||||||
:hint "no invitation associated with the token"))
|
|
||||||
|
|
||||||
|
(if (some? profile)
|
||||||
|
(if (or (= member-id profile-id)
|
||||||
|
(= member-email (:email profile)))
|
||||||
|
|
||||||
;; if we have logged-in user and it matches the invitation we proceed
|
;; if we have logged-in user and it matches the invitation we proceed
|
||||||
;; with accepting the invitation and joining the current profile to the
|
;; with accepting the invitation and joining the current profile to the
|
||||||
@ -236,16 +187,17 @@
|
|||||||
:profile-id (:id profile)
|
:profile-id (:id profile)
|
||||||
:email (:email profile))))))
|
:email (:email profile))))))
|
||||||
|
|
||||||
(let [accepted-team-id (accept-invitation cfg claims invitation profile)]
|
(accept-invitation cfg claims invitation profile)
|
||||||
(cond-> (assoc claims :state :created)
|
(assoc claims :state :created))
|
||||||
;; when the invitation is to an org, instead of a team, add the
|
|
||||||
;; accepted-team-id as :org-team-id
|
(ex/raise :type :validation
|
||||||
(:organization-id claims)
|
:code :invalid-token
|
||||||
(assoc :org-team-id accepted-team-id)))))
|
:hint "logged-in user does not matches the invitation"))
|
||||||
|
|
||||||
;; If we have not logged-in user, and invitation comes with member-id we
|
;; If we have not logged-in user, and invitation comes with member-id we
|
||||||
;; redirect user to login, if no memeber-id is present and in the invitation
|
;; redirect user to login, if no memeber-id is present and in the invitation
|
||||||
;; token and registration is enabled, we redirect user the the register page.
|
;; token and registration is enabled, we redirect user the the register page.
|
||||||
|
|
||||||
{:invitation-token token
|
{:invitation-token token
|
||||||
:iss :team-invitation
|
:iss :team-invitation
|
||||||
:redirect-to (if (or member-id registration-disabled?) :auth-login :auth-register)
|
:redirect-to (if (or member-id registration-disabled?) :auth-login :auth-register)
|
||||||
|
|||||||
@ -28,25 +28,19 @@
|
|||||||
(update :pages-index select-keys allowed)))
|
(update :pages-index select-keys allowed)))
|
||||||
|
|
||||||
(defn obfuscate-email
|
(defn obfuscate-email
|
||||||
"Obfuscate the `email` for share-link members so the viewer only sees a
|
|
||||||
partially redacted address. Accepts any string shape (including nil,
|
|
||||||
missing `@`, or a domain with no `.`) and falls back to a fully-masked
|
|
||||||
result rather than throwing — the function is called while building the
|
|
||||||
view-only bundle for anonymous viewers, so an NPE here would abort the
|
|
||||||
entire share-link response."
|
|
||||||
[email]
|
[email]
|
||||||
(let [[name domain]
|
(let [[name domain]
|
||||||
(str/split (or email "") "@" 2)
|
(str/split email "@" 2)
|
||||||
|
|
||||||
[_ rest]
|
[_ rest]
|
||||||
(str/split (or domain "") "." 2)
|
(str/split domain "." 2)
|
||||||
|
|
||||||
name
|
name
|
||||||
(if (> (count name) 3)
|
(if (> (count name) 3)
|
||||||
(str (subs name 0 1) (apply str (take (dec (count name)) (repeat "*"))))
|
(str (subs name 0 1) (apply str (take (dec (count name)) (repeat "*"))))
|
||||||
"****")]
|
"****")]
|
||||||
|
|
||||||
(str name "@****" (when rest (str "." rest)))))
|
(str name "@****." rest)))
|
||||||
|
|
||||||
(defn anonymize-member
|
(defn anonymize-member
|
||||||
[member]
|
[member]
|
||||||
|
|||||||
@ -8,33 +8,22 @@
|
|||||||
"Internal Nitrate HTTP RPC API. Provides authenticated access to
|
"Internal Nitrate HTTP RPC API. Provides authenticated access to
|
||||||
organization management and token validation endpoints."
|
organization management and token validation endpoints."
|
||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
[app.common.features :as cfeat]
|
||||||
[app.common.exceptions :as ex]
|
|
||||||
[app.common.schema :as sm]
|
[app.common.schema :as sm]
|
||||||
[app.common.types.organization :refer [schema:team-with-organization]]
|
|
||||||
[app.common.types.profile :refer [schema:profile, schema:basic-profile]]
|
[app.common.types.profile :refer [schema:profile, schema:basic-profile]]
|
||||||
[app.common.types.team :refer [schema:team]]
|
[app.common.types.team :refer [schema:team]]
|
||||||
|
[app.common.uuid :as uuid]
|
||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.media :as media]
|
[app.msgbus :as mbus]
|
||||||
[app.nitrate :as nitrate]
|
|
||||||
[app.rpc :as-alias rpc]
|
[app.rpc :as-alias rpc]
|
||||||
[app.rpc.commands.files :as files]
|
[app.rpc.commands.files :as files]
|
||||||
[app.rpc.commands.nitrate :as cnit]
|
|
||||||
[app.rpc.commands.profile :as profile]
|
[app.rpc.commands.profile :as profile]
|
||||||
[app.rpc.commands.teams :as teams]
|
[app.rpc.commands.teams :as teams]
|
||||||
[app.rpc.commands.teams-invitations :as ti]
|
|
||||||
[app.rpc.doc :as doc]
|
[app.rpc.doc :as doc]
|
||||||
[app.rpc.notifications :as notifications]
|
[app.rpc.quotes :as quotes]
|
||||||
[app.storage :as sto]
|
[app.util.services :as sv]
|
||||||
[app.util.services :as sv]))
|
[clojure.set :as set]))
|
||||||
|
|
||||||
|
|
||||||
(defn- profile-to-map [profile]
|
|
||||||
{:id (:id profile)
|
|
||||||
:name (:fullname profile)
|
|
||||||
:email (:email profile)
|
|
||||||
:photo-url (files/resolve-public-uri (get profile :photo-id))})
|
|
||||||
|
|
||||||
;; ---- API: authenticate
|
;; ---- API: authenticate
|
||||||
|
|
||||||
@ -44,9 +33,11 @@
|
|||||||
::sm/params [:map]
|
::sm/params [:map]
|
||||||
::sm/result schema:profile}
|
::sm/result schema:profile}
|
||||||
[cfg {:keys [::rpc/profile-id] :as params}]
|
[cfg {:keys [::rpc/profile-id] :as params}]
|
||||||
(let [profile (profile/get-profile cfg profile-id)]
|
(let [profile (profile/get-profile cfg profile-id)]
|
||||||
(-> (profile-to-map profile)
|
{:id (get profile :id)
|
||||||
(assoc :theme (:theme profile)))))
|
:name (get profile :fullname)
|
||||||
|
:email (get profile :email)
|
||||||
|
:photo-url (files/resolve-public-uri (get profile :photo-id))}))
|
||||||
|
|
||||||
;; ---- API: get-teams
|
;; ---- API: get-teams
|
||||||
|
|
||||||
@ -85,47 +76,29 @@
|
|||||||
(->> (db/exec! cfg [sql:get-teams current-user-id])
|
(->> (db/exec! cfg [sql:get-teams current-user-id])
|
||||||
(map #(select-keys % [:id :name])))))
|
(map #(select-keys % [:id :name])))))
|
||||||
|
|
||||||
;; ---- API: upload-org-logo
|
|
||||||
|
|
||||||
(def ^:private schema:upload-org-logo
|
|
||||||
[:map
|
|
||||||
[:content media/schema:upload]
|
|
||||||
[:organization-id ::sm/uuid]
|
|
||||||
[:previous-id {:optional true} ::sm/uuid]])
|
|
||||||
|
|
||||||
(def ^:private schema:upload-org-logo-result
|
|
||||||
[:map [:id ::sm/uuid]])
|
|
||||||
|
|
||||||
(sv/defmethod ::upload-org-logo
|
|
||||||
"Store an organization logo in penpot storage and return its ID.
|
|
||||||
Accepts an optional previous-id to mark the old logo for garbage
|
|
||||||
collection when replacing an existing one."
|
|
||||||
{::doc/added "2.17"
|
|
||||||
::sm/params schema:upload-org-logo
|
|
||||||
::sm/result schema:upload-org-logo-result}
|
|
||||||
[{:keys [::sto/storage]} {:keys [content organization-id previous-id]}]
|
|
||||||
(when previous-id
|
|
||||||
(sto/touch-object! storage previous-id))
|
|
||||||
(let [hash (sto/calculate-hash (:path content))
|
|
||||||
data (-> (sto/content (:path content))
|
|
||||||
(sto/wrap-with-hash hash))
|
|
||||||
obj (sto/put-object! storage {::sto/content data
|
|
||||||
::sto/deduplicate? true
|
|
||||||
:bucket "organization"
|
|
||||||
:content-type (:mtype content)
|
|
||||||
:organization-id organization-id})]
|
|
||||||
{:id (:id obj)}))
|
|
||||||
|
|
||||||
;; ---- API: notify-team-change
|
;; ---- API: notify-team-change
|
||||||
|
|
||||||
|
(def ^:private schema:notify-team-change
|
||||||
|
[:map
|
||||||
|
[:id ::sm/uuid]
|
||||||
|
[:organization-id ::sm/uuid]
|
||||||
|
[:organization-name ::sm/text]])
|
||||||
|
|
||||||
(sv/defmethod ::notify-team-change
|
(sv/defmethod ::notify-team-change
|
||||||
"Notify to Penpot a team change from nitrate"
|
"Notify to Penpot a team change from nitrate"
|
||||||
{::doc/added "2.14"
|
{::doc/added "2.14"
|
||||||
::sm/params schema:team-with-organization
|
::sm/params schema:notify-team-change
|
||||||
::rpc/auth false}
|
::rpc/auth false}
|
||||||
[cfg team]
|
[cfg {:keys [id organization-id organization-name]}]
|
||||||
(notifications/notify-team-change cfg (select-keys team [:id :is-your-penpot :organization]) nil)
|
(let [msgbus (::mbus/msgbus cfg)]
|
||||||
nil)
|
(mbus/pub! msgbus
|
||||||
|
;;TODO There is a bug on dashboard with teams notifications.
|
||||||
|
;;For now we send it to uuid/zero instead of team-id
|
||||||
|
:topic uuid/zero
|
||||||
|
:message {:type :team-org-change
|
||||||
|
:team-id id
|
||||||
|
:organization-id organization-id
|
||||||
|
:organization-name organization-name})))
|
||||||
|
|
||||||
;; ---- API: notify-user-added-to-organization
|
;; ---- API: notify-user-added-to-organization
|
||||||
|
|
||||||
@ -140,8 +113,18 @@
|
|||||||
{::doc/added "2.14"
|
{::doc/added "2.14"
|
||||||
::sm/params schema:notify-user-added-to-organization
|
::sm/params schema:notify-user-added-to-organization
|
||||||
::rpc/auth false}
|
::rpc/auth false}
|
||||||
[cfg {:keys [profile-id organization-id]}]
|
[cfg {:keys [profile-id]}]
|
||||||
(db/tx-run! cfg teams/create-default-org-team profile-id organization-id))
|
(quotes/check! cfg {::quotes/id ::quotes/teams-per-profile
|
||||||
|
::quotes/profile-id profile-id})
|
||||||
|
|
||||||
|
(let [features (-> (cfeat/get-enabled-features cf/flags)
|
||||||
|
(set/difference cfeat/frontend-only-features)
|
||||||
|
(set/difference cfeat/no-team-inheritable-features))
|
||||||
|
params {:profile-id profile-id
|
||||||
|
:name "Default"
|
||||||
|
:features features}
|
||||||
|
team (db/tx-run! cfg teams/create-team params)]
|
||||||
|
(select-keys team [:id])))
|
||||||
|
|
||||||
|
|
||||||
;; ---- API: get-managed-profiles
|
;; ---- API: get-managed-profiles
|
||||||
@ -175,358 +158,3 @@
|
|||||||
(let [current-user-id (-> (profile/get-profile cfg profile-id) :id)]
|
(let [current-user-id (-> (profile/get-profile cfg profile-id) :id)]
|
||||||
(db/exec! cfg [sql:get-managed-profiles current-user-id current-user-id])))
|
(db/exec! cfg [sql:get-managed-profiles current-user-id current-user-id])))
|
||||||
|
|
||||||
;; ---- API: get-teams-summary
|
|
||||||
|
|
||||||
(def ^:private sql:get-teams-summary
|
|
||||||
"SELECT t.id, t.name, t.is_default
|
|
||||||
FROM team AS t
|
|
||||||
WHERE t.id = ANY(?)
|
|
||||||
AND t.deleted_at IS NULL;")
|
|
||||||
|
|
||||||
(def ^:private sql:get-files-count
|
|
||||||
"SELECT COUNT(f.*) AS count
|
|
||||||
FROM file AS f
|
|
||||||
JOIN project AS p ON f.project_id = p.id
|
|
||||||
JOIN team AS t ON t.id = p.team_id
|
|
||||||
WHERE p.team_id = ANY(?)
|
|
||||||
AND t.deleted_at IS NULL
|
|
||||||
AND p.deleted_at IS NULL
|
|
||||||
AND f.deleted_at IS NULL;")
|
|
||||||
|
|
||||||
(def ^:private schema:get-teams-summary-params
|
|
||||||
[:map
|
|
||||||
[:ids [:or ::sm/uuid [:vector ::sm/uuid]]]])
|
|
||||||
|
|
||||||
(def ^:private schema:get-teams-summary-result
|
|
||||||
[:map
|
|
||||||
[:teams [:vector [:map
|
|
||||||
[:id ::sm/uuid]
|
|
||||||
[:name ::sm/text]
|
|
||||||
[:is-default ::sm/boolean]]]]
|
|
||||||
[:num-files ::sm/int]])
|
|
||||||
|
|
||||||
(sv/defmethod ::get-teams-summary
|
|
||||||
"Get summary information for a list of teams"
|
|
||||||
{::doc/added "2.15"
|
|
||||||
::sm/params schema:get-teams-summary-params
|
|
||||||
::sm/result schema:get-teams-summary-result}
|
|
||||||
[cfg {:keys [ids]}]
|
|
||||||
(let [;; Handle one or multiple params
|
|
||||||
ids (cond
|
|
||||||
(uuid? ids)
|
|
||||||
[ids]
|
|
||||||
|
|
||||||
(and (vector? ids) (every? uuid? ids))
|
|
||||||
ids
|
|
||||||
|
|
||||||
:else
|
|
||||||
[])]
|
|
||||||
(db/run! cfg (fn [{:keys [::db/conn]}]
|
|
||||||
(let [ids-array (db/create-array conn "uuid" ids)
|
|
||||||
teams (db/exec! conn [sql:get-teams-summary ids-array])
|
|
||||||
files-count (-> (db/exec-one! conn [sql:get-files-count ids-array]) :count)]
|
|
||||||
{:teams teams
|
|
||||||
:num-files files-count})))))
|
|
||||||
|
|
||||||
|
|
||||||
;; ---- API: delete-teams-keeping-your-penpot-projects
|
|
||||||
|
|
||||||
(def ^:private sql:prefix-teams-name-and-unset-default
|
|
||||||
"UPDATE team
|
|
||||||
SET name = ? || name,
|
|
||||||
is_default = FALSE
|
|
||||||
WHERE id = ANY(?)
|
|
||||||
RETURNING id, name;")
|
|
||||||
|
|
||||||
|
|
||||||
(def ^:private schema:notify-org-deletion
|
|
||||||
[:map
|
|
||||||
[:organization-name ::sm/text]
|
|
||||||
[:teams [:vector ::sm/uuid]]])
|
|
||||||
|
|
||||||
(sv/defmethod ::notify-org-deletion
|
|
||||||
"For a list of teams, rename them with the name of the deleted org, and notify
|
|
||||||
of the deletion to the connected users"
|
|
||||||
{::doc/added "2.15"
|
|
||||||
::sm/params schema:notify-org-deletion}
|
|
||||||
[cfg {:keys [teams organization-name]}]
|
|
||||||
(when (seq teams)
|
|
||||||
(let [org-prefix (str "[" (d/sanitize-string organization-name) "] ")]
|
|
||||||
(db/tx-run!
|
|
||||||
cfg
|
|
||||||
(fn [{:keys [::db/conn] :as cfg}]
|
|
||||||
(let [ids-array (db/create-array conn "uuid" teams)
|
|
||||||
;; Rename projects
|
|
||||||
updated-teams (db/exec! conn [sql:prefix-teams-name-and-unset-default org-prefix ids-array])]
|
|
||||||
|
|
||||||
;; Notify users
|
|
||||||
(doseq [team updated-teams]
|
|
||||||
(notifications/notify-team-change cfg {:id (:id team) :name (:name team) :organization {:name organization-name}} "dashboard.org-deleted"))))))))
|
|
||||||
|
|
||||||
;; ---- API: get-profile-by-email
|
|
||||||
|
|
||||||
(def ^:private sql:get-profile-by-email
|
|
||||||
"SELECT DISTINCT id, fullname, email, photo_id
|
|
||||||
FROM profile
|
|
||||||
WHERE email = ?
|
|
||||||
AND deleted_at IS NULL;")
|
|
||||||
|
|
||||||
(sv/defmethod ::get-profile-by-email
|
|
||||||
"Get profile by email"
|
|
||||||
{::doc/added "2.15"
|
|
||||||
::sm/params [:map [:email ::sm/email]]
|
|
||||||
::sm/result schema:profile}
|
|
||||||
[cfg {:keys [email]}]
|
|
||||||
(let [profile (db/exec-one! cfg [sql:get-profile-by-email email])]
|
|
||||||
(when-not profile
|
|
||||||
(ex/raise :type :not-found
|
|
||||||
:code :profile-not-found
|
|
||||||
:hint "profile does not exist"
|
|
||||||
:email email))
|
|
||||||
(profile-to-map profile)))
|
|
||||||
|
|
||||||
|
|
||||||
;; ---- API: get-profile-by-id
|
|
||||||
|
|
||||||
(def ^:private sql:get-profile-by-id
|
|
||||||
"SELECT DISTINCT id, fullname, email, photo_id
|
|
||||||
FROM profile
|
|
||||||
WHERE id = ?
|
|
||||||
AND deleted_at IS NULL;")
|
|
||||||
|
|
||||||
(sv/defmethod ::get-profile-by-id
|
|
||||||
"Get profile by email"
|
|
||||||
{::doc/added "2.15"
|
|
||||||
::sm/params [:map [:id ::sm/uuid]]
|
|
||||||
::sm/result schema:profile}
|
|
||||||
[cfg {:keys [id]}]
|
|
||||||
(let [profile (db/exec-one! cfg [sql:get-profile-by-id id])]
|
|
||||||
(when-not profile
|
|
||||||
(ex/raise :type :not-found
|
|
||||||
:code :profile-not-found
|
|
||||||
:hint "profile does not exist"
|
|
||||||
:id id))
|
|
||||||
(profile-to-map profile)))
|
|
||||||
|
|
||||||
|
|
||||||
;; ---- API: get-org-member-team-counts
|
|
||||||
|
|
||||||
(def ^:private sql:get-org-member-team-counts
|
|
||||||
"SELECT tpr.profile_id, COUNT(DISTINCT t.id) AS team_count
|
|
||||||
FROM team_profile_rel AS tpr
|
|
||||||
JOIN team AS t ON t.id = tpr.team_id
|
|
||||||
WHERE t.id = ANY(?)
|
|
||||||
AND t.deleted_at IS NULL
|
|
||||||
AND t.is_default IS FALSE
|
|
||||||
GROUP BY tpr.profile_id;")
|
|
||||||
|
|
||||||
(def ^:private schema:get-org-member-team-counts-params
|
|
||||||
[:map [:team-ids [:or ::sm/uuid [:vector ::sm/uuid]]]])
|
|
||||||
|
|
||||||
(def ^:private schema:get-org-member-team-counts-result
|
|
||||||
[:vector [:map
|
|
||||||
[:profile-id ::sm/uuid]
|
|
||||||
[:team-count ::sm/int]]])
|
|
||||||
|
|
||||||
(sv/defmethod ::get-org-member-team-counts
|
|
||||||
"Get the number of non-default teams each profile belongs to within a set of teams."
|
|
||||||
{::doc/added "2.15"
|
|
||||||
::sm/params schema:get-org-member-team-counts-params
|
|
||||||
::sm/result schema:get-org-member-team-counts-result
|
|
||||||
::rpc/auth false}
|
|
||||||
[cfg {:keys [team-ids]}]
|
|
||||||
(let [team-ids (cond
|
|
||||||
(uuid? team-ids)
|
|
||||||
[team-ids]
|
|
||||||
|
|
||||||
(and (vector? team-ids) (every? uuid? team-ids))
|
|
||||||
team-ids
|
|
||||||
|
|
||||||
:else
|
|
||||||
[])]
|
|
||||||
(if (empty? team-ids)
|
|
||||||
[]
|
|
||||||
(db/run! cfg (fn [{:keys [::db/conn]}]
|
|
||||||
(let [ids-array (db/create-array conn "uuid" team-ids)]
|
|
||||||
(db/exec! conn [sql:get-org-member-team-counts ids-array])))))))
|
|
||||||
|
|
||||||
|
|
||||||
;; API: invite-to-org
|
|
||||||
|
|
||||||
(sv/defmethod ::invite-to-org
|
|
||||||
"Invite to organization"
|
|
||||||
{::doc/added "2.15"
|
|
||||||
::sm/params [:map
|
|
||||||
[:email ::sm/email]
|
|
||||||
[:id ::sm/uuid]
|
|
||||||
[:name ::sm/text]
|
|
||||||
[:logo ::sm/uri]]}
|
|
||||||
[cfg params]
|
|
||||||
(db/tx-run! cfg ti/create-org-invitation params)
|
|
||||||
nil)
|
|
||||||
|
|
||||||
|
|
||||||
;; API: get-org-invitations
|
|
||||||
|
|
||||||
(def ^:private sql:get-org-invitations
|
|
||||||
"SELECT DISTINCT ON (email_to)
|
|
||||||
ti.id,
|
|
||||||
ti.org_id AS organization_id,
|
|
||||||
ti.email_to AS email,
|
|
||||||
ti.created_at AS sent_at,
|
|
||||||
p.fullname AS name,
|
|
||||||
p.photo_id
|
|
||||||
FROM team_invitation AS ti
|
|
||||||
LEFT JOIN profile AS p
|
|
||||||
ON p.email = ti.email_to
|
|
||||||
AND p.deleted_at IS NULL
|
|
||||||
WHERE ti.valid_until >= now()
|
|
||||||
AND (ti.org_id = ? OR ti.team_id = ANY(?))
|
|
||||||
ORDER BY ti.email_to, ti.valid_until DESC, ti.created_at DESC;")
|
|
||||||
|
|
||||||
(def ^:private schema:get-org-invitations-params
|
|
||||||
[:map
|
|
||||||
[:organization-id ::sm/uuid]])
|
|
||||||
|
|
||||||
(def ^:private schema:get-org-invitations-result
|
|
||||||
[:vector
|
|
||||||
[:map
|
|
||||||
[:id ::sm/uuid]
|
|
||||||
[:organization-id {:optional true} [:maybe ::sm/uuid]]
|
|
||||||
[:email ::sm/email]
|
|
||||||
[:sent-at ::sm/inst]
|
|
||||||
[:name {:optional true} [:maybe ::sm/text]]
|
|
||||||
[:photo-url {:optional true} ::sm/uri]]])
|
|
||||||
|
|
||||||
(sv/defmethod ::get-org-invitations
|
|
||||||
"Get valid invitations for an organization, returning at most one invitation per email."
|
|
||||||
{::doc/added "2.16"
|
|
||||||
::sm/params schema:get-org-invitations-params
|
|
||||||
::sm/result schema:get-org-invitations-result}
|
|
||||||
[cfg {:keys [organization-id]}]
|
|
||||||
(let [org-summary (nitrate/call cfg :get-org-summary {:organization-id organization-id})
|
|
||||||
team-ids (->> (:teams org-summary)
|
|
||||||
(map :id)
|
|
||||||
(filter uuid?)
|
|
||||||
(into []))]
|
|
||||||
(db/run! cfg (fn [{:keys [::db/conn]}]
|
|
||||||
(let [ids-array (db/create-array conn "uuid" team-ids)]
|
|
||||||
(->> (db/exec! conn [sql:get-org-invitations organization-id ids-array])
|
|
||||||
(mapv (fn [{:keys [photo-id] :as invitation}]
|
|
||||||
(cond-> (dissoc invitation :photo-id)
|
|
||||||
photo-id
|
|
||||||
(assoc :photo-url (files/resolve-public-uri photo-id)))))))))))
|
|
||||||
|
|
||||||
|
|
||||||
;; API: delete-org-invitations
|
|
||||||
|
|
||||||
(def ^:private sql:delete-org-invitations
|
|
||||||
"DELETE FROM team_invitation AS ti
|
|
||||||
WHERE ti.email_to = ?
|
|
||||||
AND (ti.org_id = ? OR ti.team_id = ANY(?));")
|
|
||||||
|
|
||||||
(def ^:private schema:delete-org-invitations-params
|
|
||||||
[:map
|
|
||||||
[:organization-id ::sm/uuid]
|
|
||||||
[:email ::sm/email]])
|
|
||||||
|
|
||||||
(sv/defmethod ::delete-org-invitations
|
|
||||||
"Delete all invitations for one email in an organization scope (org + org teams)."
|
|
||||||
{::doc/added "2.16"
|
|
||||||
::sm/params schema:delete-org-invitations-params}
|
|
||||||
[cfg {:keys [organization-id email]}]
|
|
||||||
(let [org-summary (nitrate/call cfg :get-org-summary {:organization-id organization-id})
|
|
||||||
clean-email (profile/clean-email email)
|
|
||||||
team-ids (->> (:teams org-summary)
|
|
||||||
(map :id)
|
|
||||||
(filter uuid?)
|
|
||||||
(into []))]
|
|
||||||
(db/run! cfg (fn [{:keys [::db/conn]}]
|
|
||||||
(let [ids-array (db/create-array conn "uuid" team-ids)]
|
|
||||||
(db/exec! conn [sql:delete-org-invitations clean-email organization-id ids-array]))))
|
|
||||||
nil))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
;; API: remove-from-org
|
|
||||||
|
|
||||||
(def ^:private sql:get-reassign-to
|
|
||||||
"SELECT tpr.profile_id
|
|
||||||
FROM team_profile_rel AS tpr
|
|
||||||
WHERE tpr.team_id = ?
|
|
||||||
AND tpr.profile_id <> ?
|
|
||||||
AND tpr.is_owner IS NOT TRUE
|
|
||||||
ORDER BY CASE
|
|
||||||
WHEN tpr.is_admin IS TRUE THEN 1
|
|
||||||
ELSE 2
|
|
||||||
END,
|
|
||||||
tpr.created_at,
|
|
||||||
tpr.profile_id
|
|
||||||
LIMIT 1;")
|
|
||||||
|
|
||||||
(defn add-reassign-to [cfg profile-id team-to-transfer]
|
|
||||||
(let [reassign-to (-> (db/exec-one! cfg [sql:get-reassign-to (:id team-to-transfer) profile-id])
|
|
||||||
:profile-id)]
|
|
||||||
(when-not reassign-to
|
|
||||||
(ex/raise :type :validation
|
|
||||||
:code :nobody-to-reassign-team))
|
|
||||||
|
|
||||||
(assoc team-to-transfer :reassign-to reassign-to)))
|
|
||||||
|
|
||||||
(sv/defmethod ::remove-from-org
|
|
||||||
"Remove an user from an organization"
|
|
||||||
{::doc/added "2.17"
|
|
||||||
::sm/params [:map
|
|
||||||
[:profile-id ::sm/uuid]
|
|
||||||
[:organization-id ::sm/uuid]
|
|
||||||
[:organization-name ::sm/text]
|
|
||||||
[:default-team-id ::sm/uuid]]
|
|
||||||
::db/transaction true}
|
|
||||||
[cfg {:keys [profile-id organization-id organization-name default-team-id] :as params}]
|
|
||||||
(let [{:keys [valid-teams-to-delete-ids
|
|
||||||
valid-teams-to-transfer
|
|
||||||
valid-teams-to-exit]} (cnit/get-valid-teams cfg organization-id profile-id default-team-id)
|
|
||||||
add-reassign-to (partial add-reassign-to cfg profile-id)
|
|
||||||
|
|
||||||
valid-teams-to-leave (into valid-teams-to-exit
|
|
||||||
(map add-reassign-to valid-teams-to-transfer))]
|
|
||||||
|
|
||||||
(cnit/leave-org cfg (assoc params
|
|
||||||
:id organization-id
|
|
||||||
:name organization-name
|
|
||||||
:teams-to-delete valid-teams-to-delete-ids
|
|
||||||
:teams-to-leave valid-teams-to-leave
|
|
||||||
:skip-validation true))
|
|
||||||
(notifications/notify-user-org-change cfg profile-id organization-id organization-name "dashboard.user-no-longer-belong-org")
|
|
||||||
nil))
|
|
||||||
|
|
||||||
;; API: get-remove-from-org-summary
|
|
||||||
|
|
||||||
(def ^:private schema:get-remove-from-org-summary-result
|
|
||||||
[:map
|
|
||||||
[:teams-to-delete ::sm/int]
|
|
||||||
[:teams-to-transfer ::sm/int]
|
|
||||||
[:teams-to-exit ::sm/int]])
|
|
||||||
|
|
||||||
(sv/defmethod ::get-remove-from-org-summary
|
|
||||||
"Get a summary of the teams that would be deleted, transferred, or exited
|
|
||||||
if the user were removed from the organization"
|
|
||||||
{::doc/added "2.17"
|
|
||||||
::sm/params [:map
|
|
||||||
[:profile-id ::sm/uuid]
|
|
||||||
[:organization-id ::sm/uuid]
|
|
||||||
[:default-team-id ::sm/uuid]]
|
|
||||||
::sm/result schema:get-remove-from-org-summary-result
|
|
||||||
::db/transaction true}
|
|
||||||
[cfg {:keys [profile-id organization-id default-team-id]}]
|
|
||||||
(let [{:keys [valid-teams-to-delete-ids
|
|
||||||
valid-teams-to-transfer
|
|
||||||
valid-teams-to-exit
|
|
||||||
valid-default-team]} (cnit/get-valid-teams cfg organization-id profile-id default-team-id)]
|
|
||||||
(when-not valid-default-team
|
|
||||||
(ex/raise :type :validation
|
|
||||||
:code :not-valid-teams))
|
|
||||||
{:teams-to-delete (count valid-teams-to-delete-ids)
|
|
||||||
:teams-to-transfer (count valid-teams-to-transfer)
|
|
||||||
:teams-to-exit (count valid-teams-to-exit)}))
|
|
||||||
|
|
||||||
|
|||||||
@ -1,33 +0,0 @@
|
|||||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
||||||
;;
|
|
||||||
;; Copyright (c) KALEIDOS INC
|
|
||||||
|
|
||||||
(ns app.rpc.notifications
|
|
||||||
(:require
|
|
||||||
[app.common.uuid :as uuid]
|
|
||||||
[app.msgbus :as mbus]))
|
|
||||||
|
|
||||||
(defn notify-team-change
|
|
||||||
[cfg team notification]
|
|
||||||
(let [msgbus (::mbus/msgbus cfg)]
|
|
||||||
(mbus/pub! msgbus
|
|
||||||
;;TODO There is a bug on dashboard with teams notifications.
|
|
||||||
;;For now we send it to uuid/zero instead of team-id
|
|
||||||
:topic uuid/zero
|
|
||||||
:message {:type :team-org-change
|
|
||||||
:team team
|
|
||||||
:notification notification})))
|
|
||||||
|
|
||||||
|
|
||||||
(defn notify-user-org-change
|
|
||||||
[cfg profile-id organization-id organization-name notification]
|
|
||||||
(let [msgbus (::mbus/msgbus cfg)]
|
|
||||||
(mbus/pub! msgbus
|
|
||||||
:topic profile-id
|
|
||||||
:message {:type :user-org-change
|
|
||||||
:topic profile-id
|
|
||||||
:organization-id organization-id
|
|
||||||
:organization-name organization-name
|
|
||||||
:notification notification})))
|
|
||||||
@ -53,7 +53,7 @@
|
|||||||
:or {is-active true}}]
|
:or {is-active true}}]
|
||||||
(some-> (get-current-system)
|
(some-> (get-current-system)
|
||||||
(db/tx-run!
|
(db/tx-run!
|
||||||
(fn [system]
|
(fn [{:keys [::db/conn] :as system}]
|
||||||
(let [password (derive-password password)
|
(let [password (derive-password password)
|
||||||
params {:id (uuid/next)
|
params {:id (uuid/next)
|
||||||
:email email
|
:email email
|
||||||
@ -62,7 +62,7 @@
|
|||||||
:password password
|
:password password
|
||||||
:props {}}]
|
:props {}}]
|
||||||
(->> (cmd.auth/create-profile system params)
|
(->> (cmd.auth/create-profile system params)
|
||||||
(cmd.auth/create-profile-rels system)))))))
|
(cmd.auth/create-profile-rels conn)))))))
|
||||||
|
|
||||||
(defmethod exec-command "update-profile"
|
(defmethod exec-command "update-profile"
|
||||||
[{:keys [fullname email password is-active]}]
|
[{:keys [fullname email password is-active]}]
|
||||||
|
|||||||
@ -905,4 +905,5 @@
|
|||||||
(let [params (-> rel
|
(let [params (-> rel
|
||||||
(assoc :id (uuid/next))
|
(assoc :id (uuid/next))
|
||||||
(assoc :team-id (:id team)))]
|
(assoc :team-id (:id team)))]
|
||||||
(teams/add-profile-to-team! cfg params {::db/return-keys false}))))))))
|
(db/insert! conn :team-profile-rel params
|
||||||
|
{::db/return-keys false}))))))))
|
||||||
|
|||||||
@ -44,7 +44,6 @@
|
|||||||
"file-object-thumbnail"
|
"file-object-thumbnail"
|
||||||
"file-thumbnail"
|
"file-thumbnail"
|
||||||
"profile"
|
"profile"
|
||||||
"organization"
|
|
||||||
"tempfile"
|
"tempfile"
|
||||||
"file-data"
|
"file-data"
|
||||||
"file-data-fragment"
|
"file-data-fragment"
|
||||||
|
|||||||
@ -166,7 +166,6 @@
|
|||||||
"profile" (process-objects! conn has-profile-refs? bucket objects)
|
"profile" (process-objects! conn has-profile-refs? bucket objects)
|
||||||
"file-data" (process-objects! conn has-file-data-refs? bucket objects)
|
"file-data" (process-objects! conn has-file-data-refs? bucket objects)
|
||||||
"tempfile" (process-objects! conn (constantly false) bucket objects)
|
"tempfile" (process-objects! conn (constantly false) bucket objects)
|
||||||
"organization" (process-objects! conn (constantly false) bucket objects)
|
|
||||||
(ex/raise :type :internal
|
(ex/raise :type :internal
|
||||||
:code :unexpected-unknown-reference
|
:code :unexpected-unknown-reference
|
||||||
:hint (dm/fmt "unknown reference '%'" bucket))))
|
:hint (dm/fmt "unknown reference '%'" bucket))))
|
||||||
|
|||||||
@ -30,18 +30,21 @@
|
|||||||
java.nio.file.Path
|
java.nio.file.Path
|
||||||
java.time.Duration
|
java.time.Duration
|
||||||
java.util.Collection
|
java.util.Collection
|
||||||
java.util.concurrent.atomic.AtomicLong
|
|
||||||
java.util.Optional
|
java.util.Optional
|
||||||
|
java.util.concurrent.atomic.AtomicLong
|
||||||
org.reactivestreams.Subscriber
|
org.reactivestreams.Subscriber
|
||||||
software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider
|
software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider
|
||||||
|
software.amazon.awssdk.core.ResponseBytes
|
||||||
software.amazon.awssdk.core.async.AsyncRequestBody
|
software.amazon.awssdk.core.async.AsyncRequestBody
|
||||||
software.amazon.awssdk.core.async.AsyncResponseTransformer
|
software.amazon.awssdk.core.async.AsyncResponseTransformer
|
||||||
software.amazon.awssdk.core.async.BlockingInputStreamAsyncRequestBody
|
software.amazon.awssdk.core.async.BlockingInputStreamAsyncRequestBody
|
||||||
software.amazon.awssdk.core.client.config.ClientAsyncConfiguration
|
software.amazon.awssdk.core.client.config.ClientAsyncConfiguration
|
||||||
software.amazon.awssdk.core.ResponseBytes
|
|
||||||
software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient
|
software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient
|
||||||
software.amazon.awssdk.http.nio.netty.SdkEventLoopGroup
|
software.amazon.awssdk.http.nio.netty.SdkEventLoopGroup
|
||||||
software.amazon.awssdk.regions.Region
|
software.amazon.awssdk.regions.Region
|
||||||
|
software.amazon.awssdk.services.s3.S3AsyncClient
|
||||||
|
software.amazon.awssdk.services.s3.S3AsyncClientBuilder
|
||||||
|
software.amazon.awssdk.services.s3.S3Configuration
|
||||||
software.amazon.awssdk.services.s3.model.Delete
|
software.amazon.awssdk.services.s3.model.Delete
|
||||||
software.amazon.awssdk.services.s3.model.DeleteObjectRequest
|
software.amazon.awssdk.services.s3.model.DeleteObjectRequest
|
||||||
software.amazon.awssdk.services.s3.model.DeleteObjectsRequest
|
software.amazon.awssdk.services.s3.model.DeleteObjectsRequest
|
||||||
@ -51,12 +54,9 @@
|
|||||||
software.amazon.awssdk.services.s3.model.ObjectIdentifier
|
software.amazon.awssdk.services.s3.model.ObjectIdentifier
|
||||||
software.amazon.awssdk.services.s3.model.PutObjectRequest
|
software.amazon.awssdk.services.s3.model.PutObjectRequest
|
||||||
software.amazon.awssdk.services.s3.model.S3Error
|
software.amazon.awssdk.services.s3.model.S3Error
|
||||||
software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest
|
|
||||||
software.amazon.awssdk.services.s3.presigner.model.PresignedGetObjectRequest
|
|
||||||
software.amazon.awssdk.services.s3.presigner.S3Presigner
|
software.amazon.awssdk.services.s3.presigner.S3Presigner
|
||||||
software.amazon.awssdk.services.s3.S3AsyncClient
|
software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest
|
||||||
software.amazon.awssdk.services.s3.S3AsyncClientBuilder
|
software.amazon.awssdk.services.s3.presigner.model.PresignedGetObjectRequest))
|
||||||
software.amazon.awssdk.services.s3.S3Configuration))
|
|
||||||
|
|
||||||
(def ^:private max-retries
|
(def ^:private max-retries
|
||||||
"A maximum number of retries on internal operations"
|
"A maximum number of retries on internal operations"
|
||||||
|
|||||||
@ -1,55 +0,0 @@
|
|||||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
||||||
;;
|
|
||||||
;; Copyright (c) KALEIDOS INC
|
|
||||||
|
|
||||||
(ns backend-tests.auth-oidc-test
|
|
||||||
(:require
|
|
||||||
[app.auth.oidc :as oidc]
|
|
||||||
[clojure.test :as t]))
|
|
||||||
|
|
||||||
(def ^:private oidc-provider
|
|
||||||
{:id "oidc"
|
|
||||||
:type "oidc"})
|
|
||||||
|
|
||||||
(t/deftest parse-attr-path-supports-dot-and-double-underscore
|
|
||||||
(t/is
|
|
||||||
(= [:oidc/resource-access :penpot_roles :roles]
|
|
||||||
(#'oidc/parse-attr-path oidc-provider "resource_access__penpot_roles__roles")))
|
|
||||||
(t/is
|
|
||||||
(= [:oidc/ocs :data :email]
|
|
||||||
(#'oidc/parse-attr-path oidc-provider "ocs.data.email"))))
|
|
||||||
|
|
||||||
(t/deftest process-user-info-supports-dot-notation-nested-attrs
|
|
||||||
(let [provider (assoc oidc-provider
|
|
||||||
:email-attr "ocs.data.email"
|
|
||||||
:name-attr "ocs.data.display-name")
|
|
||||||
info (#'oidc/process-user-info provider
|
|
||||||
{}
|
|
||||||
{:email_verified true
|
|
||||||
:ocs {:data {:email "nextcloud@example.com"
|
|
||||||
:display-name "Nextcloud User"}}})]
|
|
||||||
(t/is (= "nextcloud@example.com" (:email info)))
|
|
||||||
(t/is (= "Nextcloud User" (:fullname info)))
|
|
||||||
(t/is (true? (:email-verified info)))))
|
|
||||||
|
|
||||||
;; The provider's `:user-info-source` value arrives as a string (enforced by
|
|
||||||
;; the malli schema in `app.config` and used as-is by the hard-coded Google /
|
|
||||||
;; GitHub provider maps), so the dispatch must interpret strings — not
|
|
||||||
;; keywords — to actually honour `PENPOT_OIDC_USER_INFO_SOURCE=userinfo`.
|
|
||||||
(t/deftest select-user-info-source-interprets-config-strings
|
|
||||||
(t/testing "explicit string values map to keyword dispatch tokens"
|
|
||||||
(t/is (= :token (#'oidc/select-user-info-source "token")))
|
|
||||||
(t/is (= :userinfo (#'oidc/select-user-info-source "userinfo"))))
|
|
||||||
|
|
||||||
(t/testing "missing or explicit \"auto\" falls back to auto dispatch"
|
|
||||||
(t/is (= :auto (#'oidc/select-user-info-source "auto")))
|
|
||||||
(t/is (= :auto (#'oidc/select-user-info-source nil))))
|
|
||||||
|
|
||||||
(t/testing "unknown values fall back to auto dispatch safely"
|
|
||||||
(t/is (= :auto (#'oidc/select-user-info-source "unknown")))
|
|
||||||
;; Guards against the reverse regression — a stray keyword value must
|
|
||||||
;; not silently slip through as if it were the matching string.
|
|
||||||
(t/is (= :auto (#'oidc/select-user-info-source :token)))
|
|
||||||
(t/is (= :auto (#'oidc/select-user-info-source :userinfo)))))
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
||||||
;;
|
|
||||||
;; Copyright (c) KALEIDOS INC
|
|
||||||
|
|
||||||
(ns backend-tests.email-blacklist-test
|
|
||||||
(:require
|
|
||||||
[app.email :as-alias email]
|
|
||||||
[app.email.blacklist :as blacklist]
|
|
||||||
[clojure.test :as t]))
|
|
||||||
|
|
||||||
(def ^:private cfg
|
|
||||||
{::email/blacklist #{"somedomain.com" "spam.net"}})
|
|
||||||
|
|
||||||
(t/deftest test-exact-domain-match
|
|
||||||
(t/is (true? (blacklist/contains? cfg "user@somedomain.com")))
|
|
||||||
(t/is (true? (blacklist/contains? cfg "user@spam.net")))
|
|
||||||
(t/is (false? (blacklist/contains? cfg "user@legit.com"))))
|
|
||||||
|
|
||||||
(t/deftest test-subdomain-match
|
|
||||||
(t/is (true? (blacklist/contains? cfg "user@sub.somedomain.com")))
|
|
||||||
(t/is (true? (blacklist/contains? cfg "user@a.b.somedomain.com")))
|
|
||||||
;; A domain that merely contains the blacklisted string but is not a
|
|
||||||
;; subdomain must NOT be rejected.
|
|
||||||
(t/is (false? (blacklist/contains? cfg "user@notsomedomain.com"))))
|
|
||||||
|
|
||||||
(t/deftest test-case-insensitive
|
|
||||||
(t/is (true? (blacklist/contains? cfg "user@SOMEDOMAIN.COM")))
|
|
||||||
(t/is (true? (blacklist/contains? cfg "user@Sub.SomeDomain.Com"))))
|
|
||||||
|
|
||||||
(t/deftest test-non-blacklisted-domain
|
|
||||||
(t/is (false? (blacklist/contains? cfg "user@example.com")))
|
|
||||||
(t/is (false? (blacklist/contains? cfg "user@sub.legit.com"))))
|
|
||||||
@ -186,10 +186,10 @@
|
|||||||
:is-demo false}
|
:is-demo false}
|
||||||
params)]
|
params)]
|
||||||
(db/run! system
|
(db/run! system
|
||||||
(fn [cfg]
|
(fn [{:keys [::db/conn] :as cfg}]
|
||||||
(->> params
|
(->> params
|
||||||
(cmd.auth/create-profile cfg)
|
(cmd.auth/create-profile cfg)
|
||||||
(cmd.auth/create-profile-rels cfg)))))))
|
(cmd.auth/create-profile-rels conn)))))))
|
||||||
|
|
||||||
(defn create-project*
|
(defn create-project*
|
||||||
([i params] (create-project* *system* i params))
|
([i params] (create-project* *system* i params))
|
||||||
@ -234,10 +234,10 @@
|
|||||||
(dm/with-open [conn (db/open system)]
|
(dm/with-open [conn (db/open system)]
|
||||||
(let [id (mk-uuid "team" i)
|
(let [id (mk-uuid "team" i)
|
||||||
features (cfeat/get-enabled-features cf/flags)]
|
features (cfeat/get-enabled-features cf/flags)]
|
||||||
(teams/create-team {::db/conn conn} {:id id
|
(teams/create-team conn {:id id
|
||||||
:profile-id profile-id
|
:profile-id profile-id
|
||||||
:features features
|
:features features
|
||||||
:name (str "team" i)})))))
|
:name (str "team" i)})))))
|
||||||
|
|
||||||
(defn create-file-media-object*
|
(defn create-file-media-object*
|
||||||
([params] (create-file-media-object* *system* params))
|
([params] (create-file-media-object* *system* params))
|
||||||
@ -283,10 +283,9 @@
|
|||||||
([params] (create-team-role* *system* params))
|
([params] (create-team-role* *system* params))
|
||||||
([system {:keys [team-id profile-id role] :or {role :owner}}]
|
([system {:keys [team-id profile-id role] :or {role :owner}}]
|
||||||
(dm/with-open [conn (db/open system)]
|
(dm/with-open [conn (db/open system)]
|
||||||
(#'teams/create-team-role {::db/conn conn}
|
(#'teams/create-team-role conn {:team-id team-id
|
||||||
{:team-id team-id
|
:profile-id profile-id
|
||||||
:profile-id profile-id
|
:role role}))))
|
||||||
:role role}))))
|
|
||||||
|
|
||||||
(defn create-project-role*
|
(defn create-project-role*
|
||||||
([params] (create-project-role* *system* params))
|
([params] (create-project-role* *system* params))
|
||||||
@ -385,31 +384,6 @@
|
|||||||
(dissoc ::type)
|
(dissoc ::type)
|
||||||
(assoc :app.rpc/request-at (ct/now)))))))
|
(assoc :app.rpc/request-at (ct/now)))))))
|
||||||
|
|
||||||
(defn management-command!
|
|
||||||
([data]
|
|
||||||
(management-command! data nil))
|
|
||||||
([{:keys [::type] :as data} flags-to-add]
|
|
||||||
(let [flags (reduce conj cf/flags (or flags-to-add []))
|
|
||||||
|
|
||||||
resolve-management-methods
|
|
||||||
(requiring-resolve 'app.rpc/resolve-management-methods)
|
|
||||||
|
|
||||||
methods
|
|
||||||
(with-redefs [cf/flags flags]
|
|
||||||
(resolve-management-methods *system*))
|
|
||||||
|
|
||||||
[_ method-fn]
|
|
||||||
(get methods type)]
|
|
||||||
|
|
||||||
(when-not method-fn
|
|
||||||
(ex/raise :type :assertion
|
|
||||||
:code :rpc-method-not-found
|
|
||||||
:hint (str/ffmt "management rpc method '%' not found" (name type))))
|
|
||||||
|
|
||||||
(try-on! (method-fn (-> data
|
|
||||||
(dissoc ::type)
|
|
||||||
(assoc :app.rpc/request-at (ct/now))))))))
|
|
||||||
|
|
||||||
(defn run-task!
|
(defn run-task!
|
||||||
([name]
|
([name]
|
||||||
(run-task! name {}))
|
(run-task! name {}))
|
||||||
|
|||||||
@ -2121,92 +2121,3 @@
|
|||||||
(t/is (= 1 (count rows)))
|
(t/is (= 1 (count rows)))
|
||||||
(t/is (= (:created-at row1) #penpot/inst "2025-10-31T00:00:00Z"))
|
(t/is (= (:created-at row1) #penpot/inst "2025-10-31T00:00:00Z"))
|
||||||
(t/is (nil? (:deleted-at row1))))))))
|
(t/is (nil? (:deleted-at row1))))))))
|
||||||
|
|
||||||
(t/deftest get-file-stats-empty-file
|
|
||||||
(let [profile (th/create-profile* 1 {:is-active true})
|
|
||||||
file (th/create-file* 1 {:profile-id (:id profile)
|
|
||||||
:project-id (:default-project-id profile)
|
|
||||||
:is-shared false})
|
|
||||||
out (th/command! {::th/type :get-file-stats
|
|
||||||
::rpc/profile-id (:id profile)
|
|
||||||
:id (:id file)})]
|
|
||||||
|
|
||||||
;; (th/print-result! out)
|
|
||||||
(t/is (nil? (:error out)))
|
|
||||||
|
|
||||||
(let [result (:result out)]
|
|
||||||
(t/is (= (:id file) (:file-id result)))
|
|
||||||
(t/is (pos? (:page-count result)))
|
|
||||||
(t/is (zero? (:component-count result)))
|
|
||||||
(t/is (zero? (:deleted-component-count result)))
|
|
||||||
(t/is (zero? (:color-count result)))
|
|
||||||
(t/is (zero? (:typography-count result)))
|
|
||||||
(t/is (zero? (:library-count result)))
|
|
||||||
(t/is (zero? (:referenced-by-count result)))
|
|
||||||
(t/is (contains? result :shape-counts))
|
|
||||||
(t/is (zero? (get-in result [:shape-counts :total])))
|
|
||||||
(t/is (= {} (get-in result [:shape-counts :by-type]))))))
|
|
||||||
|
|
||||||
(t/deftest get-file-stats-with-shapes
|
|
||||||
(let [profile (th/create-profile* 1 {:is-active true})
|
|
||||||
file (th/create-file* 1 {:profile-id (:id profile)
|
|
||||||
:project-id (:default-project-id profile)
|
|
||||||
:is-shared false})
|
|
||||||
page-id (-> file :data :pages first)
|
|
||||||
rect-id (uuid/random)
|
|
||||||
frame-id (uuid/random)]
|
|
||||||
|
|
||||||
(update-file!
|
|
||||||
:file-id (:id file)
|
|
||||||
:profile-id (:id profile)
|
|
||||||
:revn 0
|
|
||||||
:vern 0
|
|
||||||
:changes
|
|
||||||
[{:type :add-obj
|
|
||||||
:page-id page-id
|
|
||||||
:id frame-id
|
|
||||||
:parent-id uuid/zero
|
|
||||||
:frame-id uuid/zero
|
|
||||||
:components-v2 true
|
|
||||||
:obj (cts/setup-shape
|
|
||||||
{:id frame-id
|
|
||||||
:name "frame"
|
|
||||||
:frame-id uuid/zero
|
|
||||||
:parent-id uuid/zero
|
|
||||||
:type :frame})}
|
|
||||||
{:type :add-obj
|
|
||||||
:page-id page-id
|
|
||||||
:id rect-id
|
|
||||||
:parent-id frame-id
|
|
||||||
:frame-id frame-id
|
|
||||||
:components-v2 true
|
|
||||||
:obj (cts/setup-shape
|
|
||||||
{:id rect-id
|
|
||||||
:name "rect"
|
|
||||||
:frame-id frame-id
|
|
||||||
:parent-id frame-id
|
|
||||||
:type :rect})}])
|
|
||||||
|
|
||||||
(let [out (th/command! {::th/type :get-file-stats
|
|
||||||
::rpc/profile-id (:id profile)
|
|
||||||
:id (:id file)})
|
|
||||||
result (:result out)]
|
|
||||||
|
|
||||||
(t/is (nil? (:error out)))
|
|
||||||
(t/is (= 2 (get-in result [:shape-counts :total])))
|
|
||||||
(t/is (= 1 (get-in result [:shape-counts :by-type :rect])))
|
|
||||||
(t/is (= 1 (get-in result [:shape-counts :by-type :frame]))))))
|
|
||||||
|
|
||||||
(t/deftest get-file-stats-forbidden
|
|
||||||
(let [owner (th/create-profile* 1 {:is-active true})
|
|
||||||
other (th/create-profile* 2 {:is-active true})
|
|
||||||
file (th/create-file* 1 {:profile-id (:id owner)
|
|
||||||
:project-id (:default-project-id owner)
|
|
||||||
:is-shared false})
|
|
||||||
out (th/command! {::th/type :get-file-stats
|
|
||||||
::rpc/profile-id (:id other)
|
|
||||||
:id (:id file)})]
|
|
||||||
|
|
||||||
(t/is (not (nil? (:error out))))
|
|
||||||
(let [edata (-> out :error ex-data)]
|
|
||||||
(t/is (= :not-found (:type edata))))))
|
|
||||||
|
|||||||
@ -1,800 +0,0 @@
|
|||||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
||||||
;;
|
|
||||||
;; Copyright (c) KALEIDOS INC
|
|
||||||
|
|
||||||
(ns backend-tests.rpc-management-nitrate-test
|
|
||||||
(:require
|
|
||||||
[app.common.data :as d]
|
|
||||||
[app.common.time :as ct]
|
|
||||||
[app.common.uuid :as uuid]
|
|
||||||
[app.config :as cf]
|
|
||||||
[app.db :as-alias db]
|
|
||||||
[app.email :as email]
|
|
||||||
[app.msgbus :as mbus]
|
|
||||||
[app.nitrate :as nitrate]
|
|
||||||
[app.rpc :as-alias rpc]
|
|
||||||
[backend-tests.helpers :as th]
|
|
||||||
[clojure.set :as set]
|
|
||||||
[clojure.test :as t]
|
|
||||||
[cuerdas.core :as str]))
|
|
||||||
|
|
||||||
(t/use-fixtures :once th/state-init)
|
|
||||||
(t/use-fixtures :each th/database-reset)
|
|
||||||
|
|
||||||
(defn- management-command-with-nitrate!
|
|
||||||
[data]
|
|
||||||
(th/management-command! data [:nitrate]))
|
|
||||||
|
|
||||||
(t/deftest authenticate-success
|
|
||||||
(let [profile (th/create-profile* 1 {:is-active true
|
|
||||||
:fullname "Nitrate User"})
|
|
||||||
out (management-command-with-nitrate! {::th/type :authenticate
|
|
||||||
::rpc/profile-id (:id profile)})]
|
|
||||||
(t/is (th/success? out))
|
|
||||||
(t/is (= (:id profile) (-> out :result :id)))
|
|
||||||
(t/is (= "Nitrate User" (-> out :result :name)))
|
|
||||||
(t/is (= (:email profile) (-> out :result :email)))
|
|
||||||
(t/is (nil? (-> out :result :photo-url)))))
|
|
||||||
|
|
||||||
(t/deftest authenticate-requires-authentication
|
|
||||||
(let [out (management-command-with-nitrate! {::th/type :authenticate})]
|
|
||||||
(t/is (not (th/success? out)))
|
|
||||||
(t/is (= :authentication (th/ex-type (:error out))))
|
|
||||||
(t/is (= :authentication-required (th/ex-code (:error out))))))
|
|
||||||
|
|
||||||
(t/deftest get-penpot-version
|
|
||||||
(let [profile (th/create-profile* 1 {:is-active true})
|
|
||||||
out (management-command-with-nitrate! {::th/type :get-penpot-version
|
|
||||||
::rpc/profile-id (:id profile)})]
|
|
||||||
(t/is (th/success? out))
|
|
||||||
(t/is (= cf/version (-> out :result :version)))))
|
|
||||||
|
|
||||||
(t/deftest get-teams-returns-only-owned-non-default-non-deleted
|
|
||||||
(let [profile (th/create-profile* 1 {:is-active true})
|
|
||||||
other (th/create-profile* 2 {:is-active true})
|
|
||||||
owned-team (th/create-team* 1 {:profile-id (:id profile)})
|
|
||||||
deleted-team (th/create-team* 2 {:profile-id (:id profile)})
|
|
||||||
_ (th/db-update! :team
|
|
||||||
{:deleted-at (ct/now)}
|
|
||||||
{:id (:id deleted-team)})
|
|
||||||
other-team (th/create-team* 3 {:profile-id (:id other)})
|
|
||||||
_ (th/create-team-role* {:team-id (:id other-team)
|
|
||||||
:profile-id (:id profile)
|
|
||||||
:role :editor})
|
|
||||||
out (management-command-with-nitrate! {::th/type :get-teams
|
|
||||||
::rpc/profile-id (:id profile)})]
|
|
||||||
(t/is (th/success? out))
|
|
||||||
(t/is (= #{(:id owned-team)}
|
|
||||||
(->> out :result (map :id) set)))
|
|
||||||
(t/is (= #{(:name owned-team)}
|
|
||||||
(->> out :result (map :name) set)))))
|
|
||||||
|
|
||||||
(t/deftest notify-team-change-publishes-event
|
|
||||||
(let [team-id (uuid/random)
|
|
||||||
organization-id (uuid/random)
|
|
||||||
organization {:id organization-id
|
|
||||||
:name "Acme Inc"
|
|
||||||
:slug "acme-inc"
|
|
||||||
:owner-id (uuid/random)
|
|
||||||
:avatar-bg-url "http://example.com/avatar.svg"}
|
|
||||||
calls (atom [])
|
|
||||||
out (with-redefs [mbus/pub! (fn [_cfg & {:keys [topic message]}]
|
|
||||||
(swap! calls conj {:topic topic
|
|
||||||
:message message}))]
|
|
||||||
(management-command-with-nitrate! {::th/type :notify-team-change
|
|
||||||
:id team-id
|
|
||||||
:is-your-penpot false
|
|
||||||
:organization organization}))]
|
|
||||||
(t/is (th/success? out))
|
|
||||||
(t/is (= 1 (count @calls)))
|
|
||||||
(t/is (= uuid/zero (-> @calls first :topic)))
|
|
||||||
(let [msg (-> @calls first :message)]
|
|
||||||
(t/is (= :team-org-change (:type msg)))
|
|
||||||
(t/is (= nil (:notification msg)))
|
|
||||||
(t/is (= team-id (-> msg :team :id)))
|
|
||||||
(t/is (= false (-> msg :team :is-your-penpot)))
|
|
||||||
(t/is (= (:id organization) (-> msg :team :organization :id)))
|
|
||||||
(t/is (= (:name organization) (-> msg :team :organization :name)))
|
|
||||||
(t/is (= (:slug organization) (-> msg :team :organization :slug)))
|
|
||||||
(t/is (= (:owner-id organization) (-> msg :team :organization :owner-id)))
|
|
||||||
(t/is (= (:avatar-bg-url organization) (str (-> msg :team :organization :avatar-bg-url)))))))
|
|
||||||
|
|
||||||
(t/deftest notify-user-added-to-organization-creates-default-org-team
|
|
||||||
(let [profile (th/create-profile* 1 {:is-active true})
|
|
||||||
before-teams (->> (th/db-query :team-profile-rel {:profile-id (:id profile)
|
|
||||||
:is-owner true})
|
|
||||||
(map :team-id)
|
|
||||||
set)
|
|
||||||
out (management-command-with-nitrate! {::th/type :notify-user-added-to-organization
|
|
||||||
:profile-id (:id profile)
|
|
||||||
:organization-id (uuid/random)
|
|
||||||
:role "owner"})
|
|
||||||
after-teams (->> (th/db-query :team-profile-rel {:profile-id (:id profile)
|
|
||||||
:is-owner true})
|
|
||||||
(map :team-id)
|
|
||||||
set)
|
|
||||||
new-team-id (first (set/difference after-teams before-teams))
|
|
||||||
new-team (th/db-get :team {:id new-team-id})]
|
|
||||||
(t/is (th/success? out))
|
|
||||||
(t/is (= 1 (count (set/difference after-teams before-teams))))
|
|
||||||
(t/is (= "Your Penpot" (:name new-team)))
|
|
||||||
(t/is (true? (:is-default new-team)))))
|
|
||||||
|
|
||||||
(t/deftest get-managed-profiles-returns-unique-members-for-owned-teams
|
|
||||||
(let [owner (th/create-profile* 1 {:is-active true})
|
|
||||||
member1 (th/create-profile* 2 {:is-active true})
|
|
||||||
member2 (th/create-profile* 3 {:is-active true})
|
|
||||||
team1 (th/create-team* 1 {:profile-id (:id owner)})
|
|
||||||
team2 (th/create-team* 2 {:profile-id (:id owner)})
|
|
||||||
_ (th/create-team-role* {:team-id (:id team1)
|
|
||||||
:profile-id (:id member1)
|
|
||||||
:role :editor})
|
|
||||||
_ (th/create-team-role* {:team-id (:id team1)
|
|
||||||
:profile-id (:id member2)
|
|
||||||
:role :editor})
|
|
||||||
_ (th/create-team-role* {:team-id (:id team2)
|
|
||||||
:profile-id (:id member1)
|
|
||||||
:role :editor})
|
|
||||||
out (management-command-with-nitrate! {::th/type :get-managed-profiles
|
|
||||||
::rpc/profile-id (:id owner)})]
|
|
||||||
(t/is (th/success? out))
|
|
||||||
(t/is (= #{(:id member1) (:id member2)}
|
|
||||||
(->> out :result (map :id) set)))
|
|
||||||
(t/is (= #{(:email member1) (:email member2)}
|
|
||||||
(->> out :result (map :email) set)))))
|
|
||||||
|
|
||||||
(t/deftest get-teams-summary-returns-teams-and-files-count
|
|
||||||
(let [profile (th/create-profile* 1 {:is-active true})
|
|
||||||
team1 (th/create-team* 1 {:profile-id (:id profile)})
|
|
||||||
team2 (th/create-team* 2 {:profile-id (:id profile)})
|
|
||||||
proj1 (th/create-project* 1 {:profile-id (:id profile)
|
|
||||||
:team-id (:id team1)})
|
|
||||||
proj2 (th/create-project* 2 {:profile-id (:id profile)
|
|
||||||
:team-id (:id team2)})
|
|
||||||
_ (th/create-file* 1 {:profile-id (:id profile)
|
|
||||||
:project-id (:id proj1)})
|
|
||||||
_ (th/create-file* 2 {:profile-id (:id profile)
|
|
||||||
:project-id (:id proj2)})
|
|
||||||
out (management-command-with-nitrate! {::th/type :get-teams-summary
|
|
||||||
::rpc/profile-id (:id profile)
|
|
||||||
:ids [(:id team1) (:id team2)]})]
|
|
||||||
(t/is (th/success? out))
|
|
||||||
(t/is (= 2 (-> out :result :num-files)))
|
|
||||||
(t/is (= #{(:id team1) (:id team2)}
|
|
||||||
(->> out :result :teams (map :id) set)))))
|
|
||||||
|
|
||||||
(t/deftest notify-org-deletion-prefixes-teams-and-notifies
|
|
||||||
(let [profile (th/create-profile* 1 {:is-active true})
|
|
||||||
extra-team (th/create-team* 1 {:profile-id (:id profile)})
|
|
||||||
default-team (th/db-get :team {:id (:default-team-id profile)})
|
|
||||||
teams [(:id default-team) (:id extra-team)]
|
|
||||||
organization-name "Acme / Design"
|
|
||||||
expected-start (str "[" (d/sanitize-string organization-name) "] ")
|
|
||||||
calls (atom [])
|
|
||||||
out (with-redefs [mbus/pub! (fn [_cfg & {:keys [topic message]}]
|
|
||||||
(swap! calls conj {:topic topic
|
|
||||||
:message message}))]
|
|
||||||
(management-command-with-nitrate! {::th/type :notify-org-deletion
|
|
||||||
::rpc/profile-id (:id profile)
|
|
||||||
:teams teams
|
|
||||||
:organization-name organization-name}))
|
|
||||||
updated (map #(th/db-get :team {:id %} {::db/remove-deleted false}) teams)]
|
|
||||||
(t/is (th/success? out))
|
|
||||||
(t/is (= 2 (count @calls)))
|
|
||||||
(doseq [team updated]
|
|
||||||
(t/is (false? (:is-default team)))
|
|
||||||
(t/is (str/starts-with? (:name team) expected-start)))
|
|
||||||
(doseq [call @calls]
|
|
||||||
(t/is (= uuid/zero (:topic call)))
|
|
||||||
(t/is (= :team-org-change (-> call :message :type)))
|
|
||||||
(t/is (= organization-name (-> call :message :team :organization :name)))
|
|
||||||
(t/is (= "dashboard.org-deleted" (-> call :message :notification))))))
|
|
||||||
|
|
||||||
(t/deftest get-profile-by-email-success-and-not-found
|
|
||||||
(let [profile (th/create-profile* 1 {:is-active true
|
|
||||||
:fullname "Lookup by Email"})
|
|
||||||
ok-out (management-command-with-nitrate! {::th/type :get-profile-by-email
|
|
||||||
::rpc/profile-id (:id profile)
|
|
||||||
:email (:email profile)})
|
|
||||||
ko-out (management-command-with-nitrate! {::th/type :get-profile-by-email
|
|
||||||
::rpc/profile-id (:id profile)
|
|
||||||
:email "not-found@example.com"})]
|
|
||||||
(t/is (th/success? ok-out))
|
|
||||||
(t/is (= (:id profile) (-> ok-out :result :id)))
|
|
||||||
(t/is (= "Lookup by Email" (-> ok-out :result :name)))
|
|
||||||
(t/is (nil? (-> ok-out :result :photo-url)))
|
|
||||||
|
|
||||||
(t/is (not (th/success? ko-out)))
|
|
||||||
(t/is (= :not-found (th/ex-type (:error ko-out))))
|
|
||||||
(t/is (= :profile-not-found (th/ex-code (:error ko-out))))))
|
|
||||||
|
|
||||||
(t/deftest get-profile-by-id-success-and-not-found
|
|
||||||
(let [profile (th/create-profile* 1 {:is-active true
|
|
||||||
:fullname "Lookup by Id"})
|
|
||||||
ok-out (management-command-with-nitrate! {::th/type :get-profile-by-id
|
|
||||||
::rpc/profile-id (:id profile)
|
|
||||||
:id (:id profile)})
|
|
||||||
ko-out (management-command-with-nitrate! {::th/type :get-profile-by-id
|
|
||||||
::rpc/profile-id (:id profile)
|
|
||||||
:id (uuid/random)})]
|
|
||||||
(t/is (th/success? ok-out))
|
|
||||||
(t/is (= (:id profile) (-> ok-out :result :id)))
|
|
||||||
(t/is (= "Lookup by Id" (-> ok-out :result :name)))
|
|
||||||
(t/is (nil? (-> ok-out :result :photo-url)))
|
|
||||||
|
|
||||||
(t/is (not (th/success? ko-out)))
|
|
||||||
(t/is (= :not-found (th/ex-type (:error ko-out))))
|
|
||||||
(t/is (= :profile-not-found (th/ex-code (:error ko-out))))))
|
|
||||||
|
|
||||||
(t/deftest get-org-invitations-returns-valid-deduped-by-email
|
|
||||||
(let [profile (th/create-profile* 1 {:is-active true})
|
|
||||||
team-1 (th/create-team* 1 {:profile-id (:id profile)})
|
|
||||||
team-2 (th/create-team* 2 {:profile-id (:id profile)})
|
|
||||||
org-id (uuid/random)
|
|
||||||
org-summary {:id org-id
|
|
||||||
:teams [{:id (:id team-1)}
|
|
||||||
{:id (:id team-2)}]}
|
|
||||||
params {::th/type :get-org-invitations
|
|
||||||
::rpc/profile-id (:id profile)
|
|
||||||
:organization-id org-id}]
|
|
||||||
|
|
||||||
;; Same email appears in org and team invitations; only one should be returned.
|
|
||||||
(th/db-insert! :team-invitation
|
|
||||||
{:id (uuid/random)
|
|
||||||
:org-id org-id
|
|
||||||
:team-id nil
|
|
||||||
:email-to "dup@example.com"
|
|
||||||
:created-by (:id profile)
|
|
||||||
:role "editor"
|
|
||||||
:valid-until (ct/in-future "24h")})
|
|
||||||
|
|
||||||
(th/db-insert! :team-invitation
|
|
||||||
{:id (uuid/random)
|
|
||||||
:team-id (:id team-1)
|
|
||||||
:org-id nil
|
|
||||||
:email-to "dup@example.com"
|
|
||||||
:created-by (:id profile)
|
|
||||||
:role "admin"
|
|
||||||
:valid-until (ct/in-future "72h")})
|
|
||||||
|
|
||||||
(th/db-insert! :team-invitation
|
|
||||||
{:id (uuid/random)
|
|
||||||
:team-id (:id team-2)
|
|
||||||
:org-id nil
|
|
||||||
:email-to "valid@example.com"
|
|
||||||
:created-by (:id profile)
|
|
||||||
:role "editor"
|
|
||||||
:valid-until (ct/in-future "48h")})
|
|
||||||
|
|
||||||
;; Expired invitation should be ignored.
|
|
||||||
(th/db-insert! :team-invitation
|
|
||||||
{:id (uuid/random)
|
|
||||||
:org-id org-id
|
|
||||||
:team-id nil
|
|
||||||
:email-to "expired@example.com"
|
|
||||||
:created-by (:id profile)
|
|
||||||
:role "editor"
|
|
||||||
:valid-until (ct/in-past "1h")})
|
|
||||||
|
|
||||||
(let [out (with-redefs [nitrate/call (fn [_cfg method _params]
|
|
||||||
(case method
|
|
||||||
:get-org-summary org-summary
|
|
||||||
nil))]
|
|
||||||
(management-command-with-nitrate! params))
|
|
||||||
result (:result out)
|
|
||||||
emails (->> result (map :email) set)
|
|
||||||
dedup (->> result
|
|
||||||
(filter #(= "dup@example.com" (:email %)))
|
|
||||||
first)]
|
|
||||||
(t/is (th/success? out))
|
|
||||||
(t/is (= #{"dup@example.com" "valid@example.com"} emails))
|
|
||||||
(t/is (= 2 (count result)))
|
|
||||||
(t/is (some? (:id dedup)))
|
|
||||||
(t/is (some? (:sent-at dedup)))
|
|
||||||
(t/is (nil? (:organization-id dedup)))
|
|
||||||
(t/is (nil? (:team-id dedup)))
|
|
||||||
(t/is (nil? (:role dedup)))
|
|
||||||
(t/is (nil? (:valid-until dedup))))))
|
|
||||||
|
|
||||||
(t/deftest get-org-invitations-includes-org-level-invitations-when-no-teams
|
|
||||||
(let [profile (th/create-profile* 1 {:is-active true})
|
|
||||||
org-id (uuid/random)
|
|
||||||
org-summary {:id org-id
|
|
||||||
:teams []}
|
|
||||||
params {::th/type :get-org-invitations
|
|
||||||
::rpc/profile-id (:id profile)
|
|
||||||
:organization-id org-id}]
|
|
||||||
|
|
||||||
(th/db-insert! :team-invitation
|
|
||||||
{:id (uuid/random)
|
|
||||||
:org-id org-id
|
|
||||||
:team-id nil
|
|
||||||
:email-to "org-only@example.com"
|
|
||||||
:created-by (:id profile)
|
|
||||||
:role "editor"
|
|
||||||
:valid-until (ct/in-future "24h")})
|
|
||||||
|
|
||||||
(let [out (with-redefs [nitrate/call (fn [_cfg method _params]
|
|
||||||
(case method
|
|
||||||
:get-org-summary org-summary
|
|
||||||
nil))]
|
|
||||||
(management-command-with-nitrate! params))
|
|
||||||
result (:result out)]
|
|
||||||
(t/is (th/success? out))
|
|
||||||
(t/is (= 1 (count result)))
|
|
||||||
(t/is (= "org-only@example.com" (-> result first :email)))
|
|
||||||
(t/is (some? (-> result first :sent-at))))))
|
|
||||||
|
|
||||||
(t/deftest get-org-invitations-returns-existing-profile-data
|
|
||||||
(let [profile (th/create-profile* 1 {:is-active true})
|
|
||||||
invited (th/create-profile* 2 {:is-active true
|
|
||||||
:fullname "Invited User"})
|
|
||||||
photo-id (uuid/random)
|
|
||||||
_ (th/db-insert! :storage-object {:id photo-id
|
|
||||||
:backend "assets-fs"})
|
|
||||||
_ (th/db-update! :profile {:photo-id photo-id} {:id (:id invited)})
|
|
||||||
org-id (uuid/random)
|
|
||||||
org-summary {:id org-id
|
|
||||||
:teams []}
|
|
||||||
params {::th/type :get-org-invitations
|
|
||||||
::rpc/profile-id (:id profile)
|
|
||||||
:organization-id org-id}]
|
|
||||||
|
|
||||||
(th/db-insert! :team-invitation
|
|
||||||
{:id (uuid/random)
|
|
||||||
:org-id org-id
|
|
||||||
:team-id nil
|
|
||||||
:email-to (:email invited)
|
|
||||||
:created-by (:id profile)
|
|
||||||
:role "editor"
|
|
||||||
:valid-until (ct/in-future "24h")})
|
|
||||||
|
|
||||||
(let [out (with-redefs [nitrate/call (fn [_cfg method _params]
|
|
||||||
(case method
|
|
||||||
:get-org-summary org-summary
|
|
||||||
nil))]
|
|
||||||
(management-command-with-nitrate! params))
|
|
||||||
invitation (-> out :result first)]
|
|
||||||
(t/is (th/success? out))
|
|
||||||
(t/is (= "Invited User" (:name invitation)))
|
|
||||||
(t/is (some? (:sent-at invitation)))
|
|
||||||
(t/is (str/ends-with? (:photo-url invitation)
|
|
||||||
(str "/assets/by-id/" photo-id))))))
|
|
||||||
|
|
||||||
(t/deftest delete-org-invitations-removes-org-and-org-team-invitations-for-email
|
|
||||||
(let [profile (th/create-profile* 1 {:is-active true})
|
|
||||||
team-1 (th/create-team* 1 {:profile-id (:id profile)})
|
|
||||||
team-2 (th/create-team* 2 {:profile-id (:id profile)})
|
|
||||||
outside-team (th/create-team* 3 {:profile-id (:id profile)})
|
|
||||||
org-id (uuid/random)
|
|
||||||
org-summary {:id org-id
|
|
||||||
:teams [{:id (:id team-1)}
|
|
||||||
{:id (:id team-2)}]}
|
|
||||||
target-email "target@example.com"
|
|
||||||
params {::th/type :delete-org-invitations
|
|
||||||
::rpc/profile-id (:id profile)
|
|
||||||
:organization-id org-id
|
|
||||||
:email "TARGET@example.com"}]
|
|
||||||
|
|
||||||
;; Should be deleted: org-level invitation for same org+email.
|
|
||||||
(th/db-insert! :team-invitation
|
|
||||||
{:id (uuid/random)
|
|
||||||
:org-id org-id
|
|
||||||
:team-id nil
|
|
||||||
:email-to target-email
|
|
||||||
:created-by (:id profile)
|
|
||||||
:role "editor"
|
|
||||||
:valid-until (ct/in-future "24h")})
|
|
||||||
|
|
||||||
;; Should be deleted: team-level invitation for teams belonging to org summary.
|
|
||||||
(th/db-insert! :team-invitation
|
|
||||||
{:id (uuid/random)
|
|
||||||
:team-id (:id team-1)
|
|
||||||
:org-id nil
|
|
||||||
:email-to target-email
|
|
||||||
:created-by (:id profile)
|
|
||||||
:role "editor"
|
|
||||||
:valid-until (ct/in-past "1h")})
|
|
||||||
|
|
||||||
;; Should remain: different email.
|
|
||||||
(th/db-insert! :team-invitation
|
|
||||||
{:id (uuid/random)
|
|
||||||
:team-id (:id team-2)
|
|
||||||
:org-id nil
|
|
||||||
:email-to "other@example.com"
|
|
||||||
:created-by (:id profile)
|
|
||||||
:role "editor"
|
|
||||||
:valid-until (ct/in-future "24h")})
|
|
||||||
|
|
||||||
;; Should remain: same email but outside org scope.
|
|
||||||
(th/db-insert! :team-invitation
|
|
||||||
{:id (uuid/random)
|
|
||||||
:team-id (:id outside-team)
|
|
||||||
:org-id nil
|
|
||||||
:email-to target-email
|
|
||||||
:created-by (:id profile)
|
|
||||||
:role "editor"
|
|
||||||
:valid-until (ct/in-future "24h")})
|
|
||||||
|
|
||||||
(let [out (with-redefs [nitrate/call (fn [_cfg method _params]
|
|
||||||
(case method
|
|
||||||
:get-org-summary org-summary
|
|
||||||
nil))]
|
|
||||||
(management-command-with-nitrate! params))
|
|
||||||
remaining-target (th/db-query :team-invitation {:email-to target-email})
|
|
||||||
remaining-other (th/db-query :team-invitation {:email-to "other@example.com"})]
|
|
||||||
(t/is (th/success? out))
|
|
||||||
(t/is (nil? (:result out)))
|
|
||||||
(t/is (= 1 (count remaining-target)))
|
|
||||||
(t/is (= (:id outside-team) (:team-id (first remaining-target))))
|
|
||||||
(t/is (= 1 (count remaining-other))))))
|
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
||||||
;; Tests: remove-from-org
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
||||||
|
|
||||||
(defn- make-org-summary
|
|
||||||
[& {:keys [organization-id organization-name owner-id your-penpot-teams org-teams]
|
|
||||||
:or {your-penpot-teams [] org-teams []}}]
|
|
||||||
{:id organization-id
|
|
||||||
:name organization-name
|
|
||||||
:owner-id owner-id
|
|
||||||
:teams (into
|
|
||||||
(mapv (fn [id] {:id id :is-your-penpot true}) your-penpot-teams)
|
|
||||||
(mapv (fn [id] {:id id :is-your-penpot false}) org-teams))})
|
|
||||||
|
|
||||||
(defn- nitrate-call-mock
|
|
||||||
[org-summary]
|
|
||||||
(fn [_cfg method _params]
|
|
||||||
(case method
|
|
||||||
:get-org-summary org-summary
|
|
||||||
:get-org-membership {:organization-id (:id org-summary)
|
|
||||||
:is-member true}
|
|
||||||
:remove-profile-from-org nil
|
|
||||||
nil)))
|
|
||||||
|
|
||||||
(t/deftest remove-from-org-happy-path-no-extra-teams
|
|
||||||
;; User is only in its default team (which has files); it should be
|
|
||||||
;; kept, renamed and unset as default. A notification must be sent.
|
|
||||||
(let [org-owner (th/create-profile* 1 {:is-active true})
|
|
||||||
user (th/create-profile* 2 {:is-active true})
|
|
||||||
org-team (th/create-team* 1 {:profile-id (:id user)})
|
|
||||||
project (th/create-project* 1 {:profile-id (:id user)
|
|
||||||
:team-id (:id org-team)})
|
|
||||||
_ (th/create-file* 1 {:profile-id (:id user)
|
|
||||||
:project-id (:id project)})
|
|
||||||
organization-id (uuid/random)
|
|
||||||
org-summary (make-org-summary
|
|
||||||
:organization-id organization-id
|
|
||||||
:organization-name "Acme Org"
|
|
||||||
:owner-id (:id org-owner)
|
|
||||||
:your-penpot-teams [(:id org-team)]
|
|
||||||
:org-teams [])
|
|
||||||
calls (atom [])
|
|
||||||
out (with-redefs [nitrate/call (nitrate-call-mock org-summary)
|
|
||||||
mbus/pub! (fn [_bus & {:keys [topic message]}]
|
|
||||||
(swap! calls conj {:topic topic :message message}))]
|
|
||||||
(management-command-with-nitrate!
|
|
||||||
{::th/type :remove-from-org
|
|
||||||
::rpc/profile-id (:id org-owner)
|
|
||||||
:profile-id (:id user)
|
|
||||||
:organization-id organization-id
|
|
||||||
:organization-name "Acme Org"
|
|
||||||
:default-team-id (:id org-team)}))]
|
|
||||||
(t/is (th/success? out))
|
|
||||||
(t/is (nil? (:result out)))
|
|
||||||
|
|
||||||
;; default team preserved, renamed and unset as default
|
|
||||||
(let [team (th/db-get :team {:id (:id org-team)})]
|
|
||||||
(t/is (false? (:is-default team)))
|
|
||||||
(t/is (str/starts-with? (:name team) "[Acme Org] ")))
|
|
||||||
|
|
||||||
;; exactly one notification sent to the user
|
|
||||||
(t/is (= 1 (count @calls)))
|
|
||||||
(let [msg (-> @calls first :message)]
|
|
||||||
(t/is (= :user-org-change (:type msg)))
|
|
||||||
(t/is (= (:id user) (:topic msg)))
|
|
||||||
(t/is (= organization-id (:organization-id msg)))
|
|
||||||
(t/is (= "Acme Org" (:organization-name msg)))
|
|
||||||
(t/is (= "dashboard.user-no-longer-belong-org" (:notification msg))))))
|
|
||||||
|
|
||||||
(t/deftest remove-from-org-deletes-empty-default-team
|
|
||||||
;; When the default team has no files it should be soft-deleted.
|
|
||||||
(let [org-owner (th/create-profile* 1 {:is-active true})
|
|
||||||
user (th/create-profile* 2 {:is-active true})
|
|
||||||
org-team (th/create-team* 2 {:profile-id (:id user)})
|
|
||||||
organization-id (uuid/random)
|
|
||||||
org-summary (make-org-summary
|
|
||||||
:organization-id organization-id
|
|
||||||
:organization-name "Acme Org"
|
|
||||||
:owner-id (:id org-owner)
|
|
||||||
:your-penpot-teams [(:id org-team)]
|
|
||||||
:org-teams [])
|
|
||||||
out (with-redefs [nitrate/call (nitrate-call-mock org-summary)
|
|
||||||
mbus/pub! (fn [& _] nil)]
|
|
||||||
(management-command-with-nitrate!
|
|
||||||
{::th/type :remove-from-org
|
|
||||||
::rpc/profile-id (:id org-owner)
|
|
||||||
:profile-id (:id user)
|
|
||||||
:organization-id organization-id
|
|
||||||
:organization-name "Acme Org"
|
|
||||||
:default-team-id (:id org-team)}))]
|
|
||||||
(t/is (th/success? out))
|
|
||||||
(let [team (th/db-get :team {:id (:id org-team)} {::db/remove-deleted false})]
|
|
||||||
(t/is (some? (:deleted-at team))))))
|
|
||||||
|
|
||||||
(t/deftest remove-from-org-deletes-sole-owner-team
|
|
||||||
;; When the user is the sole member of an org team it should be deleted.
|
|
||||||
(let [org-owner (th/create-profile* 1 {:is-active true})
|
|
||||||
user (th/create-profile* 2 {:is-active true})
|
|
||||||
extra-team (th/create-team* 3 {:profile-id (:id user)})
|
|
||||||
org-team (th/create-team* 99 {:profile-id (:id user)})
|
|
||||||
organization-id (uuid/random)
|
|
||||||
org-summary (make-org-summary
|
|
||||||
:organization-id organization-id
|
|
||||||
:organization-name "Acme Org"
|
|
||||||
:owner-id (:id org-owner)
|
|
||||||
:your-penpot-teams [(:id org-team)]
|
|
||||||
:org-teams [(:id extra-team)])
|
|
||||||
out (with-redefs [nitrate/call (nitrate-call-mock org-summary)
|
|
||||||
mbus/pub! (fn [& _] nil)]
|
|
||||||
(management-command-with-nitrate!
|
|
||||||
{::th/type :remove-from-org
|
|
||||||
::rpc/profile-id (:id org-owner)
|
|
||||||
:profile-id (:id user)
|
|
||||||
:organization-id organization-id
|
|
||||||
:organization-name "Acme Org"
|
|
||||||
:default-team-id (:id org-team)}))]
|
|
||||||
(t/is (th/success? out))
|
|
||||||
(let [team (th/db-get :team {:id (:id extra-team)} {::db/remove-deleted false})]
|
|
||||||
(t/is (some? (:deleted-at team))))))
|
|
||||||
|
|
||||||
(t/deftest remove-from-org-transfers-ownership-of-multi-member-team
|
|
||||||
;; When the user owns a team that has another non-owner member, ownership
|
|
||||||
;; is transferred to that member by the endpoint automatically.
|
|
||||||
(let [org-owner (th/create-profile* 1 {:is-active true})
|
|
||||||
user (th/create-profile* 2 {:is-active true})
|
|
||||||
candidate (th/create-profile* 3 {:is-active true})
|
|
||||||
extra-team (th/create-team* 4 {:profile-id (:id user)})
|
|
||||||
_ (th/create-team-role* {:team-id (:id extra-team)
|
|
||||||
:profile-id (:id candidate)
|
|
||||||
:role :editor})
|
|
||||||
org-team (th/create-team* 99 {:profile-id (:id user)})
|
|
||||||
organization-id (uuid/random)
|
|
||||||
org-summary (make-org-summary
|
|
||||||
:organization-id organization-id
|
|
||||||
:organization-name "Acme Org"
|
|
||||||
:owner-id (:id org-owner)
|
|
||||||
:your-penpot-teams [(:id org-team)]
|
|
||||||
:org-teams [(:id extra-team)])
|
|
||||||
out (with-redefs [nitrate/call (nitrate-call-mock org-summary)
|
|
||||||
mbus/pub! (fn [& _] nil)]
|
|
||||||
(management-command-with-nitrate!
|
|
||||||
{::th/type :remove-from-org
|
|
||||||
::rpc/profile-id (:id org-owner)
|
|
||||||
:profile-id (:id user)
|
|
||||||
:organization-id organization-id
|
|
||||||
:organization-name "Acme Org"
|
|
||||||
:default-team-id (:id org-team)}))]
|
|
||||||
(t/is (th/success? out))
|
|
||||||
;; user no longer in extra-team
|
|
||||||
(let [rel (th/db-get :team-profile-rel {:team-id (:id extra-team) :profile-id (:id user)})]
|
|
||||||
(t/is (nil? rel)))
|
|
||||||
;; candidate promoted to owner
|
|
||||||
(let [rel (th/db-get :team-profile-rel {:team-id (:id extra-team) :profile-id (:id candidate)})]
|
|
||||||
(t/is (true? (:is-owner rel))))))
|
|
||||||
|
|
||||||
(t/deftest remove-from-org-exits-non-owned-team
|
|
||||||
;; When the user is a non-owner member of an org team, they simply leave.
|
|
||||||
(let [org-owner (th/create-profile* 1 {:is-active true})
|
|
||||||
user (th/create-profile* 2 {:is-active true})
|
|
||||||
extra-team (th/create-team* 5 {:profile-id (:id org-owner)})
|
|
||||||
_ (th/create-team-role* {:team-id (:id extra-team)
|
|
||||||
:profile-id (:id user)
|
|
||||||
:role :editor})
|
|
||||||
org-team (th/create-team* 99 {:profile-id (:id user)})
|
|
||||||
organization-id (uuid/random)
|
|
||||||
org-summary (make-org-summary
|
|
||||||
:organization-id organization-id
|
|
||||||
:organization-name "Acme Org"
|
|
||||||
:owner-id (:id org-owner)
|
|
||||||
:your-penpot-teams [(:id org-team)]
|
|
||||||
:org-teams [(:id extra-team)])
|
|
||||||
out (with-redefs [nitrate/call (nitrate-call-mock org-summary)
|
|
||||||
mbus/pub! (fn [& _] nil)]
|
|
||||||
(management-command-with-nitrate!
|
|
||||||
{::th/type :remove-from-org
|
|
||||||
::rpc/profile-id (:id org-owner)
|
|
||||||
:profile-id (:id user)
|
|
||||||
:organization-id organization-id
|
|
||||||
:organization-name "Acme Org"
|
|
||||||
:default-team-id (:id org-team)}))]
|
|
||||||
(t/is (th/success? out))
|
|
||||||
;; user no longer a member of extra-team
|
|
||||||
(let [rel (th/db-get :team-profile-rel {:team-id (:id extra-team) :profile-id (:id user)})]
|
|
||||||
(t/is (nil? rel)))
|
|
||||||
;; team still exists for the owner
|
|
||||||
(let [team (th/db-get :team {:id (:id extra-team)})]
|
|
||||||
(t/is (some? team)))))
|
|
||||||
|
|
||||||
(t/deftest remove-from-org-error-nobody-to-reassign
|
|
||||||
;; When the user owns a multi-member team but every other member is
|
|
||||||
;; also an owner, the auto-selection query finds nobody and raises.
|
|
||||||
(let [other-owner (th/create-profile* 1 {:is-active true})
|
|
||||||
user (th/create-profile* 2 {:is-active true})
|
|
||||||
extra-team (th/create-team* 6 {:profile-id (:id user)})
|
|
||||||
;; add other-owner to the team and make them co-owner directly in DB
|
|
||||||
_ (th/create-team-role* {:team-id (:id extra-team)
|
|
||||||
:profile-id (:id other-owner)
|
|
||||||
:role :editor})
|
|
||||||
_ (th/db-update! :team-profile-rel
|
|
||||||
{:is-owner true :is-admin false}
|
|
||||||
{:team-id (:id extra-team) :profile-id (:id other-owner)})
|
|
||||||
org-team (th/create-team* 99 {:profile-id (:id user)})
|
|
||||||
organization-id (uuid/random)
|
|
||||||
org-summary (make-org-summary
|
|
||||||
:organization-id organization-id
|
|
||||||
:organization-name "Acme Org"
|
|
||||||
:owner-id (:id other-owner)
|
|
||||||
:your-penpot-teams [(:id org-team)]
|
|
||||||
:org-teams [(:id extra-team)])
|
|
||||||
out (with-redefs [nitrate/call (nitrate-call-mock org-summary)
|
|
||||||
mbus/pub! (fn [& _] nil)]
|
|
||||||
(management-command-with-nitrate!
|
|
||||||
{::th/type :remove-from-org
|
|
||||||
::rpc/profile-id (:id other-owner)
|
|
||||||
:profile-id (:id user)
|
|
||||||
:organization-id organization-id
|
|
||||||
:organization-name "Acme Org"
|
|
||||||
:default-team-id (:id org-team)}))]
|
|
||||||
(t/is (not (th/success? out)))
|
|
||||||
(t/is (= :validation (th/ex-type (:error out))))
|
|
||||||
(t/is (= :nobody-to-reassign-team (th/ex-code (:error out))))))
|
|
||||||
|
|
||||||
;; Tests: get-remove-from-org-summary
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
||||||
|
|
||||||
(t/deftest get-remove-from-org-summary-no-extra-teams
|
|
||||||
;; User only has a default team — nothing to delete/transfer/exit.
|
|
||||||
(let [org-owner (th/create-profile* 1 {:is-active true})
|
|
||||||
user (th/create-profile* 2 {:is-active true})
|
|
||||||
org-team (th/create-team* 1 {:profile-id (:id user)})
|
|
||||||
organization-id (uuid/random)
|
|
||||||
org-summary (make-org-summary
|
|
||||||
:organization-id organization-id
|
|
||||||
:organization-name "Acme Org"
|
|
||||||
:owner-id (:id org-owner)
|
|
||||||
:your-penpot-teams [(:id org-team)]
|
|
||||||
:org-teams [])
|
|
||||||
out (with-redefs [nitrate/call (nitrate-call-mock org-summary)]
|
|
||||||
(management-command-with-nitrate!
|
|
||||||
{::th/type :get-remove-from-org-summary
|
|
||||||
::rpc/profile-id (:id org-owner)
|
|
||||||
:profile-id (:id user)
|
|
||||||
:organization-id organization-id
|
|
||||||
:default-team-id (:id org-team)}))]
|
|
||||||
(t/is (th/success? out))
|
|
||||||
(t/is (= {:teams-to-delete 0
|
|
||||||
:teams-to-transfer 0
|
|
||||||
:teams-to-exit 0}
|
|
||||||
(:result out)))))
|
|
||||||
|
|
||||||
(t/deftest get-remove-from-org-summary-with-teams-to-delete
|
|
||||||
;; User owns a sole-member extra org team → 1 to delete.
|
|
||||||
(let [org-owner (th/create-profile* 1 {:is-active true})
|
|
||||||
user (th/create-profile* 2 {:is-active true})
|
|
||||||
extra-team (th/create-team* 3 {:profile-id (:id user)})
|
|
||||||
org-team (th/create-team* 99 {:profile-id (:id user)})
|
|
||||||
organization-id (uuid/random)
|
|
||||||
org-summary (make-org-summary
|
|
||||||
:organization-id organization-id
|
|
||||||
:organization-name "Acme Org"
|
|
||||||
:owner-id (:id org-owner)
|
|
||||||
:your-penpot-teams [(:id org-team)]
|
|
||||||
:org-teams [(:id extra-team)])
|
|
||||||
out (with-redefs [nitrate/call (nitrate-call-mock org-summary)]
|
|
||||||
(management-command-with-nitrate!
|
|
||||||
{::th/type :get-remove-from-org-summary
|
|
||||||
::rpc/profile-id (:id org-owner)
|
|
||||||
:profile-id (:id user)
|
|
||||||
:organization-id organization-id
|
|
||||||
:default-team-id (:id org-team)}))]
|
|
||||||
(t/is (th/success? out))
|
|
||||||
(t/is (= {:teams-to-delete 1
|
|
||||||
:teams-to-transfer 0
|
|
||||||
:teams-to-exit 0}
|
|
||||||
(:result out)))))
|
|
||||||
|
|
||||||
(t/deftest get-remove-from-org-summary-with-teams-to-transfer
|
|
||||||
;; User owns a multi-member extra org team → 1 to transfer.
|
|
||||||
(let [org-owner (th/create-profile* 1 {:is-active true})
|
|
||||||
user (th/create-profile* 2 {:is-active true})
|
|
||||||
candidate (th/create-profile* 3 {:is-active true})
|
|
||||||
extra-team (th/create-team* 4 {:profile-id (:id user)})
|
|
||||||
_ (th/create-team-role* {:team-id (:id extra-team)
|
|
||||||
:profile-id (:id candidate)
|
|
||||||
:role :editor})
|
|
||||||
org-team (th/create-team* 99 {:profile-id (:id user)})
|
|
||||||
organization-id (uuid/random)
|
|
||||||
org-summary (make-org-summary
|
|
||||||
:organization-id organization-id
|
|
||||||
:organization-name "Acme Org"
|
|
||||||
:owner-id (:id org-owner)
|
|
||||||
:your-penpot-teams [(:id org-team)]
|
|
||||||
:org-teams [(:id extra-team)])
|
|
||||||
out (with-redefs [nitrate/call (nitrate-call-mock org-summary)]
|
|
||||||
(management-command-with-nitrate!
|
|
||||||
{::th/type :get-remove-from-org-summary
|
|
||||||
::rpc/profile-id (:id org-owner)
|
|
||||||
:profile-id (:id user)
|
|
||||||
:organization-id organization-id
|
|
||||||
:default-team-id (:id org-team)}))]
|
|
||||||
(t/is (th/success? out))
|
|
||||||
(t/is (= {:teams-to-delete 0
|
|
||||||
:teams-to-transfer 1
|
|
||||||
:teams-to-exit 0}
|
|
||||||
(:result out)))))
|
|
||||||
|
|
||||||
(t/deftest get-remove-from-org-summary-with-teams-to-exit
|
|
||||||
;; User is a non-owner member of an org team → 1 to exit.
|
|
||||||
(let [org-owner (th/create-profile* 1 {:is-active true})
|
|
||||||
user (th/create-profile* 2 {:is-active true})
|
|
||||||
extra-team (th/create-team* 5 {:profile-id (:id org-owner)})
|
|
||||||
_ (th/create-team-role* {:team-id (:id extra-team)
|
|
||||||
:profile-id (:id user)
|
|
||||||
:role :editor})
|
|
||||||
org-team (th/create-team* 99 {:profile-id (:id user)})
|
|
||||||
organization-id (uuid/random)
|
|
||||||
org-summary (make-org-summary
|
|
||||||
:organization-id organization-id
|
|
||||||
:organization-name "Acme Org"
|
|
||||||
:owner-id (:id org-owner)
|
|
||||||
:your-penpot-teams [(:id org-team)]
|
|
||||||
:org-teams [(:id extra-team)])
|
|
||||||
out (with-redefs [nitrate/call (nitrate-call-mock org-summary)]
|
|
||||||
(management-command-with-nitrate!
|
|
||||||
{::th/type :get-remove-from-org-summary
|
|
||||||
::rpc/profile-id (:id org-owner)
|
|
||||||
:profile-id (:id user)
|
|
||||||
:organization-id organization-id
|
|
||||||
:default-team-id (:id org-team)}))]
|
|
||||||
(t/is (th/success? out))
|
|
||||||
(t/is (= {:teams-to-delete 0
|
|
||||||
:teams-to-transfer 0
|
|
||||||
:teams-to-exit 1}
|
|
||||||
(:result out)))))
|
|
||||||
|
|
||||||
(t/deftest get-remove-from-org-summary-does-not-mutate
|
|
||||||
;; Calling the summary endpoint must not modify any teams.
|
|
||||||
(let [org-owner (th/create-profile* 1 {:is-active true})
|
|
||||||
user (th/create-profile* 2 {:is-active true})
|
|
||||||
extra-team (th/create-team* 6 {:profile-id (:id user)})
|
|
||||||
org-team (th/create-team* 99 {:profile-id (:id user)})
|
|
||||||
organization-id (uuid/random)
|
|
||||||
org-summary (make-org-summary
|
|
||||||
:organization-id organization-id
|
|
||||||
:organization-name "Acme Org"
|
|
||||||
:owner-id (:id org-owner)
|
|
||||||
:your-penpot-teams [(:id org-team)]
|
|
||||||
:org-teams [(:id extra-team)])
|
|
||||||
_ (with-redefs [nitrate/call (nitrate-call-mock org-summary)]
|
|
||||||
(management-command-with-nitrate!
|
|
||||||
{::th/type :get-remove-from-org-summary
|
|
||||||
::rpc/profile-id (:id org-owner)
|
|
||||||
:profile-id (:id user)
|
|
||||||
:organization-id organization-id
|
|
||||||
:default-team-id (:id org-team)}))]
|
|
||||||
;; Both teams must still exist and be undeleted
|
|
||||||
(let [t1 (th/db-get :team {:id (:id org-team)})]
|
|
||||||
(t/is (some? t1))
|
|
||||||
(t/is (nil? (:deleted-at t1))))
|
|
||||||
(let [t2 (th/db-get :team {:id (:id extra-team)})]
|
|
||||||
(t/is (some? t2))
|
|
||||||
(t/is (nil? (:deleted-at t2))))
|
|
||||||
;; User must still be a member of both teams
|
|
||||||
(let [rel1 (th/db-get :team-profile-rel {:team-id (:id org-team) :profile-id (:id user)})]
|
|
||||||
(t/is (some? rel1)))
|
|
||||||
(let [rel2 (th/db-get :team-profile-rel {:team-id (:id extra-team) :profile-id (:id user)})]
|
|
||||||
(t/is (some? rel2)))))
|
|
||||||
@ -1,686 +0,0 @@
|
|||||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
||||||
;;
|
|
||||||
;; Copyright (c) KALEIDOS INC
|
|
||||||
|
|
||||||
(ns backend-tests.rpc-nitrate-test
|
|
||||||
(:require
|
|
||||||
[app.common.uuid :as uuid]
|
|
||||||
[app.db :as-alias db]
|
|
||||||
[app.nitrate :as nitrate]
|
|
||||||
[app.rpc :as-alias rpc]
|
|
||||||
[app.rpc.commands.nitrate]
|
|
||||||
[backend-tests.helpers :as th]
|
|
||||||
[clojure.test :as t]
|
|
||||||
[cuerdas.core :as str]))
|
|
||||||
|
|
||||||
(t/use-fixtures :once th/state-init)
|
|
||||||
(t/use-fixtures :each th/database-reset)
|
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
||||||
;; Helpers
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
||||||
|
|
||||||
(defn- make-org-summary
|
|
||||||
[& {:keys [organization-id organization-name owner-id your-penpot-teams org-teams]
|
|
||||||
:or {your-penpot-teams [] org-teams []}}]
|
|
||||||
{:id organization-id
|
|
||||||
:name organization-name
|
|
||||||
:owner-id owner-id
|
|
||||||
:teams (into
|
|
||||||
(mapv (fn [id] {:id id :is-your-penpot true}) your-penpot-teams)
|
|
||||||
(mapv (fn [id] {:id id :is-your-penpot false}) org-teams))})
|
|
||||||
|
|
||||||
(defn- nitrate-call-mock
|
|
||||||
"Creates a mock for nitrate/call that returns the given org-summary for
|
|
||||||
:get-org-summary, a valid membership for :get-org-membership, and nil for
|
|
||||||
any other method."
|
|
||||||
[org-summary]
|
|
||||||
(fn [_cfg method _params]
|
|
||||||
(case method
|
|
||||||
:get-org-summary org-summary
|
|
||||||
:get-org-membership {:is-member true
|
|
||||||
:organization-id (:id org-summary)}
|
|
||||||
nil)))
|
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
||||||
;; Tests
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
||||||
|
|
||||||
(t/deftest leave-org-happy-path-no-extra-teams
|
|
||||||
(let [profile-owner (th/create-profile* 1 {:is-active true})
|
|
||||||
profile-user (th/create-profile* 2 {:is-active true})
|
|
||||||
|
|
||||||
org-default-team (th/create-team* 99 {:profile-id (:id profile-user)})
|
|
||||||
project (th/create-project* 99 {:profile-id (:id profile-user)
|
|
||||||
:team-id (:id org-default-team)})
|
|
||||||
_ (th/create-file* 99 {:profile-id (:id profile-user)
|
|
||||||
:project-id (:id project)})
|
|
||||||
|
|
||||||
organization-id (uuid/random)
|
|
||||||
;; The user's personal penpot team in the org context
|
|
||||||
your-penpot-id (:id org-default-team)
|
|
||||||
|
|
||||||
org-summary (make-org-summary
|
|
||||||
:organization-id organization-id
|
|
||||||
:organization-name "Test Org"
|
|
||||||
:owner-id (:id profile-owner)
|
|
||||||
:your-penpot-teams [your-penpot-id]
|
|
||||||
:org-teams [])]
|
|
||||||
|
|
||||||
(with-redefs [nitrate/call (nitrate-call-mock org-summary)]
|
|
||||||
(let [data {::th/type :leave-org
|
|
||||||
::rpc/profile-id (:id profile-user)
|
|
||||||
:id organization-id
|
|
||||||
:name "Test Org"
|
|
||||||
:default-team-id your-penpot-id
|
|
||||||
:teams-to-delete []
|
|
||||||
:teams-to-leave []}
|
|
||||||
out (th/command! data)]
|
|
||||||
|
|
||||||
;; (th/print-result! out)
|
|
||||||
(t/is (th/success? out))
|
|
||||||
(t/is (nil? (:result out)))
|
|
||||||
|
|
||||||
;; The personal team must be renamed with the org prefix and
|
|
||||||
;; unset as a default team.
|
|
||||||
(let [team (th/db-get :team {:id your-penpot-id})]
|
|
||||||
(t/is (str/starts-with? (:name team) "[Test Org] "))
|
|
||||||
(t/is (false? (:is-default team))))))))
|
|
||||||
|
|
||||||
(t/deftest leave-org-deletes-org-default-team-when-empty
|
|
||||||
(let [profile-owner (th/create-profile* 1 {:is-active true})
|
|
||||||
profile-user (th/create-profile* 2 {:is-active true})
|
|
||||||
org-default-team (th/create-team* 98 {:profile-id (:id profile-user)})
|
|
||||||
|
|
||||||
organization-id (uuid/random)
|
|
||||||
your-penpot-id (:id org-default-team)
|
|
||||||
|
|
||||||
org-summary (make-org-summary
|
|
||||||
:organization-id organization-id
|
|
||||||
:organization-name "Test Org"
|
|
||||||
:owner-id (:id profile-owner)
|
|
||||||
:your-penpot-teams [your-penpot-id]
|
|
||||||
:org-teams [])]
|
|
||||||
|
|
||||||
(with-redefs [nitrate/call (nitrate-call-mock org-summary)]
|
|
||||||
(let [data {::th/type :leave-org
|
|
||||||
::rpc/profile-id (:id profile-user)
|
|
||||||
:id organization-id
|
|
||||||
:name "Test Org"
|
|
||||||
:default-team-id your-penpot-id
|
|
||||||
:teams-to-delete []
|
|
||||||
:teams-to-leave []}
|
|
||||||
out (th/command! data)]
|
|
||||||
|
|
||||||
(t/is (th/success? out))
|
|
||||||
|
|
||||||
;; Empty org default team should be soft-deleted.
|
|
||||||
(let [team (th/db-get :team {:id your-penpot-id} {::db/remove-deleted false})]
|
|
||||||
(t/is (some? (:deleted-at team))))))))
|
|
||||||
|
|
||||||
(t/deftest leave-org-keeps-and-renames-org-default-team-when-has-files
|
|
||||||
(let [profile-owner (th/create-profile* 1 {:is-active true})
|
|
||||||
profile-user (th/create-profile* 2 {:is-active true})
|
|
||||||
org-default-team (th/create-team* 97 {:profile-id (:id profile-user)})
|
|
||||||
project (th/create-project* 97 {:profile-id (:id profile-user)
|
|
||||||
:team-id (:id org-default-team)})
|
|
||||||
_ (th/create-file* 97 {:profile-id (:id profile-user)
|
|
||||||
:project-id (:id project)})
|
|
||||||
|
|
||||||
organization-id (uuid/random)
|
|
||||||
your-penpot-id (:id org-default-team)
|
|
||||||
|
|
||||||
org-summary (make-org-summary
|
|
||||||
:organization-id organization-id
|
|
||||||
:organization-name "Test Org"
|
|
||||||
:owner-id (:id profile-owner)
|
|
||||||
:your-penpot-teams [your-penpot-id]
|
|
||||||
:org-teams [])]
|
|
||||||
|
|
||||||
(with-redefs [nitrate/call (nitrate-call-mock org-summary)]
|
|
||||||
(let [data {::th/type :leave-org
|
|
||||||
::rpc/profile-id (:id profile-user)
|
|
||||||
:id organization-id
|
|
||||||
:name "Test Org"
|
|
||||||
:default-team-id your-penpot-id
|
|
||||||
:teams-to-delete []
|
|
||||||
:teams-to-leave []}
|
|
||||||
out (th/command! data)]
|
|
||||||
|
|
||||||
(t/is (th/success? out))
|
|
||||||
|
|
||||||
;; Non-empty org default team should remain and be renamed.
|
|
||||||
(let [team (th/db-get :team {:id your-penpot-id})]
|
|
||||||
(t/is (str/starts-with? (:name team) "[Test Org] "))
|
|
||||||
(t/is (false? (:is-default team)))
|
|
||||||
(t/is (nil? (:deleted-at team))))))))
|
|
||||||
|
|
||||||
(t/deftest leave-org-with-teams-to-delete
|
|
||||||
(let [profile-owner (th/create-profile* 1 {:is-active true})
|
|
||||||
profile-user (th/create-profile* 2 {:is-active true})
|
|
||||||
;; profile-user is the sole owner/member of team1
|
|
||||||
team1 (th/create-team* 1 {:profile-id (:id profile-user)})
|
|
||||||
org-default-team (th/create-team* 99 {:profile-id (:id profile-user)})
|
|
||||||
|
|
||||||
organization-id (uuid/random)
|
|
||||||
your-penpot-id (:id org-default-team)
|
|
||||||
|
|
||||||
org-summary (make-org-summary
|
|
||||||
:organization-id organization-id
|
|
||||||
:organization-name "Test Org"
|
|
||||||
:owner-id (:id profile-owner)
|
|
||||||
:your-penpot-teams [your-penpot-id]
|
|
||||||
:org-teams [(:id team1)])]
|
|
||||||
|
|
||||||
(with-redefs [nitrate/call (nitrate-call-mock org-summary)]
|
|
||||||
(let [data {::th/type :leave-org
|
|
||||||
::rpc/profile-id (:id profile-user)
|
|
||||||
:id organization-id
|
|
||||||
:name "Test Org"
|
|
||||||
:default-team-id your-penpot-id
|
|
||||||
:teams-to-delete [(:id team1)]
|
|
||||||
:teams-to-leave []}
|
|
||||||
out (th/command! data)]
|
|
||||||
|
|
||||||
;; (th/print-result! out)
|
|
||||||
(t/is (th/success? out))
|
|
||||||
|
|
||||||
;; team1 should be scheduled for deletion (deleted-at set)
|
|
||||||
(let [team (th/db-get :team {:id (:id team1)} {::db/remove-deleted false})]
|
|
||||||
(t/is (some? (:deleted-at team))))))))
|
|
||||||
|
|
||||||
(t/deftest leave-org-with-ownership-transfer
|
|
||||||
(let [profile-owner (th/create-profile* 1 {:is-active true})
|
|
||||||
profile-user (th/create-profile* 2 {:is-active true})
|
|
||||||
;; profile-user owns team1; profile-owner is also a member
|
|
||||||
team1 (th/create-team* 1 {:profile-id (:id profile-user)})
|
|
||||||
_ (th/create-team-role* {:team-id (:id team1)
|
|
||||||
:profile-id (:id profile-owner)
|
|
||||||
:role :editor})
|
|
||||||
org-default-team (th/create-team* 99 {:profile-id (:id profile-user)})
|
|
||||||
|
|
||||||
organization-id (uuid/random)
|
|
||||||
your-penpot-id (:id org-default-team)
|
|
||||||
|
|
||||||
org-summary (make-org-summary
|
|
||||||
:organization-id organization-id
|
|
||||||
:organization-name "Test Org"
|
|
||||||
:owner-id (:id profile-owner)
|
|
||||||
:your-penpot-teams [your-penpot-id]
|
|
||||||
:org-teams [(:id team1)])]
|
|
||||||
|
|
||||||
(with-redefs [nitrate/call (nitrate-call-mock org-summary)]
|
|
||||||
(let [data {::th/type :leave-org
|
|
||||||
::rpc/profile-id (:id profile-user)
|
|
||||||
:id organization-id
|
|
||||||
:name "Test Org"
|
|
||||||
:default-team-id your-penpot-id
|
|
||||||
:teams-to-delete []
|
|
||||||
:teams-to-leave [{:id (:id team1) :reassign-to (:id profile-owner)}]}
|
|
||||||
out (th/command! data)]
|
|
||||||
|
|
||||||
;; (th/print-result! out)
|
|
||||||
(t/is (th/success? out))
|
|
||||||
|
|
||||||
;; profile-user should no longer be a member of team1
|
|
||||||
(let [rel (th/db-get :team-profile-rel
|
|
||||||
{:team-id (:id team1)
|
|
||||||
:profile-id (:id profile-user)})]
|
|
||||||
(t/is (nil? rel)))
|
|
||||||
|
|
||||||
;; profile-owner should have been promoted to owner
|
|
||||||
(let [rel (th/db-get :team-profile-rel
|
|
||||||
{:team-id (:id team1)
|
|
||||||
:profile-id (:id profile-owner)})]
|
|
||||||
(t/is (true? (:is-owner rel))))))))
|
|
||||||
|
|
||||||
(t/deftest leave-org-exit-as-non-owner
|
|
||||||
(let [profile-owner (th/create-profile* 1 {:is-active true})
|
|
||||||
profile-user (th/create-profile* 2 {:is-active true})
|
|
||||||
;; profile-owner owns team1; profile-user is a non-owner member
|
|
||||||
team1 (th/create-team* 1 {:profile-id (:id profile-owner)})
|
|
||||||
_ (th/create-team-role* {:team-id (:id team1)
|
|
||||||
:profile-id (:id profile-user)
|
|
||||||
:role :editor})
|
|
||||||
org-default-team (th/create-team* 99 {:profile-id (:id profile-user)})
|
|
||||||
|
|
||||||
organization-id (uuid/random)
|
|
||||||
your-penpot-id (:id org-default-team)
|
|
||||||
|
|
||||||
org-summary (make-org-summary
|
|
||||||
:organization-id organization-id
|
|
||||||
:organization-name "Test Org"
|
|
||||||
:owner-id (:id profile-owner)
|
|
||||||
:your-penpot-teams [your-penpot-id]
|
|
||||||
:org-teams [(:id team1)])]
|
|
||||||
|
|
||||||
(with-redefs [nitrate/call (nitrate-call-mock org-summary)]
|
|
||||||
(let [data {::th/type :leave-org
|
|
||||||
::rpc/profile-id (:id profile-user)
|
|
||||||
:id organization-id
|
|
||||||
:name "Test Org"
|
|
||||||
:default-team-id your-penpot-id
|
|
||||||
:teams-to-delete []
|
|
||||||
:teams-to-leave [{:id (:id team1)}]}
|
|
||||||
out (th/command! data)]
|
|
||||||
|
|
||||||
;; (th/print-result! out)
|
|
||||||
(t/is (th/success? out))
|
|
||||||
|
|
||||||
;; profile-user should no longer be a member of team1
|
|
||||||
(let [rel (th/db-get :team-profile-rel
|
|
||||||
{:team-id (:id team1)
|
|
||||||
:profile-id (:id profile-user)})]
|
|
||||||
(t/is (nil? rel)))
|
|
||||||
|
|
||||||
;; The team itself should still exist
|
|
||||||
(let [team (th/db-get :team {:id (:id team1)})]
|
|
||||||
(t/is (nil? (:deleted-at team))))))))
|
|
||||||
|
|
||||||
(t/deftest leave-org-error-org-owner-cannot-leave
|
|
||||||
(let [profile-owner (th/create-profile* 1 {:is-active true})
|
|
||||||
org-default-team (th/create-team* 99 {:profile-id (:id profile-owner)})
|
|
||||||
organization-id (uuid/random)
|
|
||||||
your-penpot-id (:id org-default-team)
|
|
||||||
|
|
||||||
;; profile-owner IS the org owner in the org-summary
|
|
||||||
org-summary (make-org-summary
|
|
||||||
:organization-id organization-id
|
|
||||||
:organization-name "Test Org"
|
|
||||||
:owner-id (:id profile-owner)
|
|
||||||
:your-penpot-teams [your-penpot-id]
|
|
||||||
:org-teams [])]
|
|
||||||
|
|
||||||
(with-redefs [nitrate/call (nitrate-call-mock org-summary)]
|
|
||||||
(let [data {::th/type :leave-org
|
|
||||||
::rpc/profile-id (:id profile-owner)
|
|
||||||
:id organization-id
|
|
||||||
:name "Test Org"
|
|
||||||
:default-team-id your-penpot-id
|
|
||||||
:teams-to-delete []
|
|
||||||
:teams-to-leave []}
|
|
||||||
out (th/command! data)]
|
|
||||||
|
|
||||||
(t/is (not (th/success? out)))
|
|
||||||
(t/is (= :validation (th/ex-type (:error out))))
|
|
||||||
(t/is (= :org-owner-cannot-leave (th/ex-code (:error out))))))))
|
|
||||||
|
|
||||||
(t/deftest leave-org-error-invalid-default-team-id
|
|
||||||
(let [profile-owner (th/create-profile* 1 {:is-active true})
|
|
||||||
profile-user (th/create-profile* 2 {:is-active true})
|
|
||||||
org-default-team (th/create-team* 99 {:profile-id (:id profile-user)})
|
|
||||||
organization-id (uuid/random)
|
|
||||||
your-penpot-id (:id org-default-team)
|
|
||||||
|
|
||||||
org-summary (make-org-summary
|
|
||||||
:organization-id organization-id
|
|
||||||
:organization-name "Test Org"
|
|
||||||
:owner-id (:id profile-owner)
|
|
||||||
:your-penpot-teams [your-penpot-id]
|
|
||||||
:org-teams [])]
|
|
||||||
|
|
||||||
(with-redefs [nitrate/call (nitrate-call-mock org-summary)]
|
|
||||||
;; Pass a random UUID that is not in the your-penpot-teams list
|
|
||||||
(let [data {::th/type :leave-org
|
|
||||||
::rpc/profile-id (:id profile-user)
|
|
||||||
:id organization-id
|
|
||||||
:name "Test Org"
|
|
||||||
:default-team-id (uuid/random)
|
|
||||||
:teams-to-delete []
|
|
||||||
:teams-to-leave []}
|
|
||||||
out (th/command! data)]
|
|
||||||
|
|
||||||
(t/is (not (th/success? out)))
|
|
||||||
(t/is (= :validation (th/ex-type (:error out))))
|
|
||||||
(t/is (= :not-valid-teams (th/ex-code (:error out))))))))
|
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
||||||
;; Unit Tests for calculate-valid-teams
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
||||||
|
|
||||||
(def ^:private calculate-valid-teams
|
|
||||||
(or (ns-resolve 'app.rpc.commands.nitrate 'calculate-valid-teams)
|
|
||||||
(throw (ex-info "Unable to resolve calculate-valid-teams"
|
|
||||||
{:ns 'app.rpc.commands.nitrate
|
|
||||||
:symbol 'calculate-valid-teams}))))
|
|
||||||
|
|
||||||
(defn- make-team [id & {:keys [is-owner num-members member-ids]
|
|
||||||
:or {is-owner false num-members 1 member-ids []}}]
|
|
||||||
{:id id :is-owner is-owner :num-members num-members :member-ids member-ids})
|
|
||||||
|
|
||||||
(t/deftest calculate-valid-teams-no-org-teams
|
|
||||||
(let [default-id (uuid/random)
|
|
||||||
default-team (make-team default-id)
|
|
||||||
result (calculate-valid-teams [default-team] default-id)]
|
|
||||||
(t/is (= default-team (:valid-default-team result)))
|
|
||||||
(t/is (empty? (:valid-teams-to-delete-ids result)))
|
|
||||||
(t/is (empty? (:valid-teams-to-transfer result)))
|
|
||||||
(t/is (empty? (:valid-teams-to-exit result)))))
|
|
||||||
|
|
||||||
(t/deftest calculate-valid-teams-default-not-found
|
|
||||||
(let [default-id (uuid/random)
|
|
||||||
other-id (uuid/random)
|
|
||||||
other-team (make-team other-id)
|
|
||||||
;; default-id is not in org-teams at all
|
|
||||||
result (calculate-valid-teams [other-team] default-id)]
|
|
||||||
(t/is (nil? (:valid-default-team result)))))
|
|
||||||
|
|
||||||
(t/deftest calculate-valid-teams-sole-owner-team
|
|
||||||
(let [default-id (uuid/random)
|
|
||||||
team-id (uuid/random)
|
|
||||||
default (make-team default-id)
|
|
||||||
solo-team (make-team team-id :is-owner true :num-members 1)
|
|
||||||
result (calculate-valid-teams [default solo-team] default-id)]
|
|
||||||
(t/is (contains? (:valid-teams-to-delete-ids result) team-id))
|
|
||||||
(t/is (empty? (:valid-teams-to-transfer result)))
|
|
||||||
(t/is (empty? (:valid-teams-to-exit result)))))
|
|
||||||
|
|
||||||
(t/deftest calculate-valid-teams-owned-multi-member-team
|
|
||||||
(let [default-id (uuid/random)
|
|
||||||
team-id (uuid/random)
|
|
||||||
default (make-team default-id)
|
|
||||||
;; owner of a team with 3 members — must be transferred
|
|
||||||
multi-team (make-team team-id :is-owner true :num-members 3)
|
|
||||||
result (calculate-valid-teams [default multi-team] default-id)]
|
|
||||||
(t/is (empty? (:valid-teams-to-delete-ids result)))
|
|
||||||
(t/is (= [team-id] (map :id (:valid-teams-to-transfer result))))
|
|
||||||
(t/is (empty? (:valid-teams-to-exit result)))))
|
|
||||||
|
|
||||||
(t/deftest calculate-valid-teams-non-owner-multi-member-team
|
|
||||||
(let [default-id (uuid/random)
|
|
||||||
team-id (uuid/random)
|
|
||||||
default (make-team default-id)
|
|
||||||
;; non-owner member of a team with 2 members — can just exit
|
|
||||||
exit-team (make-team team-id :is-owner false :num-members 2)
|
|
||||||
result (calculate-valid-teams [default exit-team] default-id)]
|
|
||||||
(t/is (empty? (:valid-teams-to-delete-ids result)))
|
|
||||||
(t/is (empty? (:valid-teams-to-transfer result)))
|
|
||||||
(t/is (= [team-id] (map :id (:valid-teams-to-exit result))))))
|
|
||||||
|
|
||||||
(t/deftest calculate-valid-teams-mixed
|
|
||||||
(let [default-id (uuid/random)
|
|
||||||
solo-id (uuid/random)
|
|
||||||
transfer-id (uuid/random)
|
|
||||||
exit-id (uuid/random)
|
|
||||||
default (make-team default-id)
|
|
||||||
solo-team (make-team solo-id :is-owner true :num-members 1)
|
|
||||||
transfer-team (make-team transfer-id :is-owner true :num-members 2)
|
|
||||||
exit-team (make-team exit-id :is-owner false :num-members 3)
|
|
||||||
result (calculate-valid-teams [default solo-team transfer-team exit-team] default-id)]
|
|
||||||
(t/is (= #{solo-id} (:valid-teams-to-delete-ids result)))
|
|
||||||
(t/is (= [transfer-id] (map :id (:valid-teams-to-transfer result))))
|
|
||||||
(t/is (= [exit-id] (map :id (:valid-teams-to-exit result))))
|
|
||||||
(t/is (= default-id (:id (:valid-default-team result))))))
|
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
||||||
;; Integration: combined delete + leave
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
||||||
|
|
||||||
(t/deftest leave-org-combined-delete-and-leave
|
|
||||||
(let [profile-owner (th/create-profile* 1 {:is-active true})
|
|
||||||
profile-user (th/create-profile* 2 {:is-active true})
|
|
||||||
;; team1: profile-user is sole owner — must delete
|
|
||||||
team1 (th/create-team* 1 {:profile-id (:id profile-user)})
|
|
||||||
;; team2: profile-user owns it, profile-owner is also member — must transfer
|
|
||||||
team2 (th/create-team* 2 {:profile-id (:id profile-user)})
|
|
||||||
_ (th/create-team-role* {:team-id (:id team2)
|
|
||||||
:profile-id (:id profile-owner)
|
|
||||||
:role :editor})
|
|
||||||
;; team3: profile-owner owns it, profile-user is non-owner member — can exit
|
|
||||||
team3 (th/create-team* 3 {:profile-id (:id profile-owner)})
|
|
||||||
_ (th/create-team-role* {:team-id (:id team3)
|
|
||||||
:profile-id (:id profile-user)
|
|
||||||
:role :editor})
|
|
||||||
org-default-team (th/create-team* 99 {:profile-id (:id profile-user)})
|
|
||||||
|
|
||||||
organization-id (uuid/random)
|
|
||||||
your-penpot-id (:id org-default-team)
|
|
||||||
|
|
||||||
org-summary (make-org-summary
|
|
||||||
:organization-id organization-id
|
|
||||||
:organization-name "Test Org"
|
|
||||||
:owner-id (:id profile-owner)
|
|
||||||
:your-penpot-teams [your-penpot-id]
|
|
||||||
:org-teams [(:id team1) (:id team2) (:id team3)])]
|
|
||||||
|
|
||||||
(with-redefs [nitrate/call (nitrate-call-mock org-summary)]
|
|
||||||
(let [data {::th/type :leave-org
|
|
||||||
::rpc/profile-id (:id profile-user)
|
|
||||||
:id organization-id
|
|
||||||
:name "Test Org"
|
|
||||||
:default-team-id your-penpot-id
|
|
||||||
:teams-to-delete [(:id team1)]
|
|
||||||
:teams-to-leave [{:id (:id team2) :reassign-to (:id profile-owner)}
|
|
||||||
{:id (:id team3)}]}
|
|
||||||
out (th/command! data)]
|
|
||||||
|
|
||||||
(t/is (th/success? out))
|
|
||||||
|
|
||||||
;; team1 should be soft-deleted
|
|
||||||
(let [team (th/db-get :team {:id (:id team1)} {::db/remove-deleted false})]
|
|
||||||
(t/is (some? (:deleted-at team))))
|
|
||||||
|
|
||||||
;; profile-user should no longer be a member of team2
|
|
||||||
(let [rel (th/db-get :team-profile-rel {:team-id (:id team2) :profile-id (:id profile-user)})]
|
|
||||||
(t/is (nil? rel)))
|
|
||||||
|
|
||||||
;; profile-owner should now own team2
|
|
||||||
(let [rel (th/db-get :team-profile-rel {:team-id (:id team2) :profile-id (:id profile-owner)})]
|
|
||||||
(t/is (true? (:is-owner rel))))
|
|
||||||
|
|
||||||
;; profile-user should no longer be a member of team3
|
|
||||||
(let [rel (th/db-get :team-profile-rel {:team-id (:id team3) :profile-id (:id profile-user)})]
|
|
||||||
(t/is (nil? rel)))
|
|
||||||
|
|
||||||
;; team3 itself should still exist (profile-owner is still there)
|
|
||||||
(let [team (th/db-get :team {:id (:id team3)})]
|
|
||||||
(t/is (some? team)))))))
|
|
||||||
(t/deftest leave-org-error-teams-to-delete-incomplete
|
|
||||||
(let [profile-owner (th/create-profile* 1 {:is-active true})
|
|
||||||
profile-user (th/create-profile* 2 {:is-active true})
|
|
||||||
;; profile-user is the sole owner/member of both team1 and team2
|
|
||||||
team1 (th/create-team* 1 {:profile-id (:id profile-user)})
|
|
||||||
team2 (th/create-team* 2 {:profile-id (:id profile-user)})
|
|
||||||
org-default-team (th/create-team* 99 {:profile-id (:id profile-user)})
|
|
||||||
|
|
||||||
organization-id (uuid/random)
|
|
||||||
your-penpot-id (:id org-default-team)
|
|
||||||
|
|
||||||
org-summary (make-org-summary
|
|
||||||
:organization-id organization-id
|
|
||||||
:organization-name "Test Org"
|
|
||||||
:owner-id (:id profile-owner)
|
|
||||||
:your-penpot-teams [your-penpot-id]
|
|
||||||
:org-teams [(:id team1) (:id team2)])]
|
|
||||||
|
|
||||||
(with-redefs [nitrate/call (nitrate-call-mock org-summary)]
|
|
||||||
;; Only team1 is listed; team2 is also a sole-owner team and must be included
|
|
||||||
(let [data {::th/type :leave-org
|
|
||||||
::rpc/profile-id (:id profile-user)
|
|
||||||
:id organization-id
|
|
||||||
:name "Test Org"
|
|
||||||
:default-team-id your-penpot-id
|
|
||||||
:teams-to-delete [(:id team1)]
|
|
||||||
:teams-to-leave []}
|
|
||||||
out (th/command! data)]
|
|
||||||
|
|
||||||
(t/is (not (th/success? out)))
|
|
||||||
(t/is (= :validation (th/ex-type (:error out))))
|
|
||||||
(t/is (= :not-valid-teams (th/ex-code (:error out))))))))
|
|
||||||
|
|
||||||
(t/deftest leave-org-error-cannot-delete-multi-member-team
|
|
||||||
(let [profile-owner (th/create-profile* 1 {:is-active true})
|
|
||||||
profile-user (th/create-profile* 2 {:is-active true})
|
|
||||||
;; team1 has two members: profile-user (owner) and profile-owner (editor)
|
|
||||||
team1 (th/create-team* 1 {:profile-id (:id profile-user)})
|
|
||||||
_ (th/create-team-role* {:team-id (:id team1)
|
|
||||||
:profile-id (:id profile-owner)
|
|
||||||
:role :editor})
|
|
||||||
org-default-team (th/create-team* 99 {:profile-id (:id profile-user)})
|
|
||||||
|
|
||||||
organization-id (uuid/random)
|
|
||||||
your-penpot-id (:id org-default-team)
|
|
||||||
|
|
||||||
org-summary (make-org-summary
|
|
||||||
:organization-id organization-id
|
|
||||||
:organization-name "Test Org"
|
|
||||||
:owner-id (:id profile-owner)
|
|
||||||
:your-penpot-teams [your-penpot-id]
|
|
||||||
:org-teams [(:id team1)])]
|
|
||||||
|
|
||||||
(with-redefs [nitrate/call (nitrate-call-mock org-summary)]
|
|
||||||
;; team1 has 2 members so it is not a valid deletion candidate
|
|
||||||
(let [data {::th/type :leave-org
|
|
||||||
::rpc/profile-id (:id profile-user)
|
|
||||||
:id organization-id
|
|
||||||
:name "Test Org"
|
|
||||||
:default-team-id your-penpot-id
|
|
||||||
:teams-to-delete [(:id team1)]
|
|
||||||
:teams-to-leave []}
|
|
||||||
out (th/command! data)]
|
|
||||||
|
|
||||||
(t/is (not (th/success? out)))
|
|
||||||
(t/is (= :validation (th/ex-type (:error out))))
|
|
||||||
(t/is (= :not-valid-teams (th/ex-code (:error out))))))))
|
|
||||||
|
|
||||||
(t/deftest leave-org-error-teams-to-leave-incomplete
|
|
||||||
(let [profile-owner (th/create-profile* 1 {:is-active true})
|
|
||||||
profile-user (th/create-profile* 2 {:is-active true})
|
|
||||||
;; profile-user owns team1, which also has profile-owner as editor
|
|
||||||
team1 (th/create-team* 1 {:profile-id (:id profile-user)})
|
|
||||||
_ (th/create-team-role* {:team-id (:id team1)
|
|
||||||
:profile-id (:id profile-owner)
|
|
||||||
:role :editor})
|
|
||||||
org-default-team (th/create-team* 99 {:profile-id (:id profile-user)})
|
|
||||||
|
|
||||||
organization-id (uuid/random)
|
|
||||||
your-penpot-id (:id org-default-team)
|
|
||||||
|
|
||||||
org-summary (make-org-summary
|
|
||||||
:organization-id organization-id
|
|
||||||
:organization-name "Test Org"
|
|
||||||
:owner-id (:id profile-owner)
|
|
||||||
:your-penpot-teams [your-penpot-id]
|
|
||||||
:org-teams [(:id team1)])]
|
|
||||||
|
|
||||||
(with-redefs [nitrate/call (nitrate-call-mock org-summary)]
|
|
||||||
;; team1 must be transferred (owner + multiple members) but is absent
|
|
||||||
(let [data {::th/type :leave-org
|
|
||||||
::rpc/profile-id (:id profile-user)
|
|
||||||
:id organization-id
|
|
||||||
:name "Test Org"
|
|
||||||
:default-team-id your-penpot-id
|
|
||||||
:teams-to-delete []
|
|
||||||
:teams-to-leave []}
|
|
||||||
out (th/command! data)]
|
|
||||||
|
|
||||||
(t/is (not (th/success? out)))
|
|
||||||
(t/is (= :validation (th/ex-type (:error out))))
|
|
||||||
(t/is (= :not-valid-teams (th/ex-code (:error out))))))))
|
|
||||||
|
|
||||||
(t/deftest leave-org-error-reassign-to-self
|
|
||||||
(let [profile-owner (th/create-profile* 1 {:is-active true})
|
|
||||||
profile-user (th/create-profile* 2 {:is-active true})
|
|
||||||
team1 (th/create-team* 1 {:profile-id (:id profile-user)})
|
|
||||||
_ (th/create-team-role* {:team-id (:id team1)
|
|
||||||
:profile-id (:id profile-owner)
|
|
||||||
:role :editor})
|
|
||||||
org-default-team (th/create-team* 99 {:profile-id (:id profile-user)})
|
|
||||||
|
|
||||||
organization-id (uuid/random)
|
|
||||||
your-penpot-id (:id org-default-team)
|
|
||||||
|
|
||||||
org-summary (make-org-summary
|
|
||||||
:organization-id organization-id
|
|
||||||
:organization-name "Test Org"
|
|
||||||
:owner-id (:id profile-owner)
|
|
||||||
:your-penpot-teams [your-penpot-id]
|
|
||||||
:org-teams [(:id team1)])]
|
|
||||||
|
|
||||||
(with-redefs [nitrate/call (nitrate-call-mock org-summary)]
|
|
||||||
;; reassign-to points to the profile that is leaving — not allowed
|
|
||||||
(let [data {::th/type :leave-org
|
|
||||||
::rpc/profile-id (:id profile-user)
|
|
||||||
:id organization-id
|
|
||||||
:name "Test Org"
|
|
||||||
:default-team-id your-penpot-id
|
|
||||||
:teams-to-delete []
|
|
||||||
:teams-to-leave [{:id (:id team1) :reassign-to (:id profile-user)}]}
|
|
||||||
out (th/command! data)]
|
|
||||||
|
|
||||||
(t/is (not (th/success? out)))
|
|
||||||
(t/is (= :validation (th/ex-type (:error out))))
|
|
||||||
(t/is (= :not-valid-teams (th/ex-code (:error out))))))))
|
|
||||||
|
|
||||||
(t/deftest leave-org-error-reassign-to-non-member
|
|
||||||
(let [profile-owner (th/create-profile* 1 {:is-active true})
|
|
||||||
profile-user (th/create-profile* 2 {:is-active true})
|
|
||||||
profile-other (th/create-profile* 3 {:is-active true})
|
|
||||||
;; team1 has profile-user (owner) and profile-owner (editor) — NOT profile-other
|
|
||||||
team1 (th/create-team* 1 {:profile-id (:id profile-user)})
|
|
||||||
_ (th/create-team-role* {:team-id (:id team1)
|
|
||||||
:profile-id (:id profile-owner)
|
|
||||||
:role :editor})
|
|
||||||
org-default-team (th/create-team* 99 {:profile-id (:id profile-user)})
|
|
||||||
|
|
||||||
organization-id (uuid/random)
|
|
||||||
your-penpot-id (:id org-default-team)
|
|
||||||
|
|
||||||
org-summary (make-org-summary
|
|
||||||
:organization-id organization-id
|
|
||||||
:organization-name "Test Org"
|
|
||||||
:owner-id (:id profile-owner)
|
|
||||||
:your-penpot-teams [your-penpot-id]
|
|
||||||
:org-teams [(:id team1)])]
|
|
||||||
|
|
||||||
(with-redefs [nitrate/call (nitrate-call-mock org-summary)]
|
|
||||||
;; profile-other is not a member of team1
|
|
||||||
(let [data {::th/type :leave-org
|
|
||||||
::rpc/profile-id (:id profile-user)
|
|
||||||
:id organization-id
|
|
||||||
:name "Test Org"
|
|
||||||
:default-team-id your-penpot-id
|
|
||||||
:teams-to-delete []
|
|
||||||
:teams-to-leave [{:id (:id team1) :reassign-to (:id profile-other)}]}
|
|
||||||
out (th/command! data)]
|
|
||||||
|
|
||||||
(t/is (not (th/success? out)))
|
|
||||||
(t/is (= :validation (th/ex-type (:error out))))
|
|
||||||
(t/is (= :not-valid-teams (th/ex-code (:error out))))))))
|
|
||||||
|
|
||||||
(t/deftest leave-org-error-reassign-on-non-owned-team
|
|
||||||
(let [profile-owner (th/create-profile* 1 {:is-active true})
|
|
||||||
profile-user (th/create-profile* 2 {:is-active true})
|
|
||||||
;; profile-owner owns team1; profile-user is just a non-owner member
|
|
||||||
team1 (th/create-team* 1 {:profile-id (:id profile-owner)})
|
|
||||||
_ (th/create-team-role* {:team-id (:id team1)
|
|
||||||
:profile-id (:id profile-user)
|
|
||||||
:role :editor})
|
|
||||||
org-default-team (th/create-team* 99 {:profile-id (:id profile-user)})
|
|
||||||
|
|
||||||
organization-id (uuid/random)
|
|
||||||
your-penpot-id (:id org-default-team)
|
|
||||||
|
|
||||||
org-summary (make-org-summary
|
|
||||||
:organization-id organization-id
|
|
||||||
:organization-name "Test Org"
|
|
||||||
:owner-id (:id profile-owner)
|
|
||||||
:your-penpot-teams [your-penpot-id]
|
|
||||||
:org-teams [(:id team1)])]
|
|
||||||
|
|
||||||
(with-redefs [nitrate/call (nitrate-call-mock org-summary)]
|
|
||||||
;; profile-user is not the owner so providing reassign-to is invalid
|
|
||||||
(let [data {::th/type :leave-org
|
|
||||||
::rpc/profile-id (:id profile-user)
|
|
||||||
:id organization-id
|
|
||||||
:name "Test Org"
|
|
||||||
:default-team-id your-penpot-id
|
|
||||||
:teams-to-delete []
|
|
||||||
:teams-to-leave [{:id (:id team1) :reassign-to (:id profile-owner)}]}
|
|
||||||
out (th/command! data)]
|
|
||||||
|
|
||||||
(t/is (not (th/success? out)))
|
|
||||||
(t/is (= :validation (th/ex-type (:error out))))
|
|
||||||
(t/is (= :not-valid-teams (th/ex-code (:error out))))))))
|
|
||||||
@ -125,20 +125,7 @@
|
|||||||
out (th/command! data)]
|
out (th/command! data)]
|
||||||
|
|
||||||
;; (th/print-result! out)
|
;; (th/print-result! out)
|
||||||
(t/is (nil? (:error out)))))
|
(t/is (nil? (:error out)))))))
|
||||||
|
|
||||||
(t/testing "delete photo clears photo-id"
|
|
||||||
(let [data {::th/type :delete-profile-photo
|
|
||||||
::rpc/profile-id (:id profile)}
|
|
||||||
out (th/command! data)]
|
|
||||||
(t/is (nil? (:error out)))
|
|
||||||
(t/is (nil? (:result out))))
|
|
||||||
|
|
||||||
(let [data {::th/type :get-profile
|
|
||||||
::rpc/profile-id (:id profile)}
|
|
||||||
out (th/command! data)]
|
|
||||||
(t/is (nil? (:error out)))
|
|
||||||
(t/is (nil? (:photo-id (:result out))))))))
|
|
||||||
|
|
||||||
(t/deftest profile-deletion-1
|
(t/deftest profile-deletion-1
|
||||||
(let [prof (th/create-profile* 1)
|
(let [prof (th/create-profile* 1)
|
||||||
|
|||||||
@ -9,7 +9,6 @@
|
|||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.rpc :as-alias rpc]
|
[app.rpc :as-alias rpc]
|
||||||
[app.rpc.commands.viewer :as viewer]
|
|
||||||
[backend-tests.helpers :as th]
|
[backend-tests.helpers :as th]
|
||||||
[clojure.test :as t]
|
[clojure.test :as t]
|
||||||
[datoteka.fs :as fs]))
|
[datoteka.fs :as fs]))
|
||||||
@ -17,28 +16,6 @@
|
|||||||
(t/use-fixtures :once th/state-init)
|
(t/use-fixtures :once th/state-init)
|
||||||
(t/use-fixtures :each th/database-reset)
|
(t/use-fixtures :each th/database-reset)
|
||||||
|
|
||||||
(t/deftest obfuscate-email-happy-path
|
|
||||||
(t/is (= "a****@****.com" (viewer/obfuscate-email "alice@example.com")))
|
|
||||||
(t/is (= "a****@****.example.com" (viewer/obfuscate-email "alice@sub.example.com")))
|
|
||||||
(t/is (= "****@****.com" (viewer/obfuscate-email "bob@bar.com"))))
|
|
||||||
|
|
||||||
(t/deftest obfuscate-email-handles-domain-without-dot
|
|
||||||
;; `localhost`-style domains have no `.`; the previous implementation produced
|
|
||||||
;; a dangling-dot output like "a****@****." — now the trailing `.` is only
|
|
||||||
;; emitted when there actually is a TLD segment to append.
|
|
||||||
(t/is (= "a****@****" (viewer/obfuscate-email "alice@localhost")))
|
|
||||||
(t/is (= "****@****" (viewer/obfuscate-email "x@y"))))
|
|
||||||
|
|
||||||
(t/deftest obfuscate-email-handles-malformed-input
|
|
||||||
;; These shapes must not throw — `obfuscate-email` runs while building the
|
|
||||||
;; view-only bundle for share-link viewers and an NPE here aborts the whole
|
|
||||||
;; RPC response. The previous implementation called `clojure.string/split`
|
|
||||||
;; on `nil` for the `no-@` case, raising NullPointerException.
|
|
||||||
(t/is (= "****@****" (viewer/obfuscate-email nil)))
|
|
||||||
(t/is (= "****@****" (viewer/obfuscate-email "")))
|
|
||||||
(t/is (= "r***@****" (viewer/obfuscate-email "root"))) ; no `@`, count > 3
|
|
||||||
(t/is (= "****@****" (viewer/obfuscate-email "bob")))) ; no `@`, count <= 3
|
|
||||||
|
|
||||||
(t/deftest retrieve-bundle
|
(t/deftest retrieve-bundle
|
||||||
(let [prof (th/create-profile* 1 {:is-active true})
|
(let [prof (th/create-profile* 1 {:is-active true})
|
||||||
prof2 (th/create-profile* 2 {:is-active true})
|
prof2 (th/create-profile* 2 {:is-active true})
|
||||||
|
|||||||
@ -1120,71 +1120,6 @@
|
|||||||
(when (num? value)
|
(when (num? value)
|
||||||
(format-precision value precision)))))
|
(format-precision value precision)))))
|
||||||
|
|
||||||
(defn- natural-sort-key
|
|
||||||
"Splits a string into a sequence of alternating string and number segments,
|
|
||||||
converting numeric segments to longs/ints so they compare by value rather
|
|
||||||
than lexicographically. e.g. \"size10b\" => (\"size\" 10 \"b\")"
|
|
||||||
[s]
|
|
||||||
(map (fn [part]
|
|
||||||
(if (re-matches #"\d+" part)
|
|
||||||
#?(:clj (Long/parseLong part)
|
|
||||||
:cljs (js/parseInt part))
|
|
||||||
part))
|
|
||||||
(re-seq #"\d+|\D+" s)))
|
|
||||||
|
|
||||||
(defn- natural-compare
|
|
||||||
"Comparator that orders strings naturally, sorting numeric segments by value
|
|
||||||
rather than lexicographically. Returns a negative number, zero, or positive
|
|
||||||
number when a is before, equal to, or after b respectively.
|
|
||||||
e.g. \"size2\" < \"size10\" instead of \"size10\" < \"size2\"."
|
|
||||||
[a b]
|
|
||||||
(loop [ka (natural-sort-key a)
|
|
||||||
kb (natural-sort-key b)]
|
|
||||||
(cond
|
|
||||||
(and (empty? ka) (empty? kb)) 0
|
|
||||||
(empty? ka) -1
|
|
||||||
(empty? kb) 1
|
|
||||||
:else
|
|
||||||
(let [pa (first ka)
|
|
||||||
pb (first kb)
|
|
||||||
result (cond
|
|
||||||
(and (number? pa) (number? pb)) (compare pa pb)
|
|
||||||
(and (string? pa) (string? pb)) (compare pa pb)
|
|
||||||
(number? pa) -1
|
|
||||||
:else 1)]
|
|
||||||
(if (zero? result)
|
|
||||||
(recur (rest ka) (rest kb))
|
|
||||||
result)))))
|
|
||||||
|
|
||||||
(defn natural-sort-by
|
|
||||||
"Sorts coll by extracting a string key with keyfn and ordering elements
|
|
||||||
using natural sort order, where embedded numbers are compared by value
|
|
||||||
rather than lexicographically.
|
|
||||||
e.g. (natural-sort-by :name [{:name \"size10\"} {:name \"size2\"}])
|
|
||||||
=> [{:name \"size2\"} {:name \"size10\"}]"
|
|
||||||
[key coll]
|
|
||||||
(sort-by key natural-compare coll))
|
|
||||||
|
|
||||||
(defn sanitize-string [s]
|
|
||||||
(if s
|
|
||||||
(-> s
|
|
||||||
str
|
|
||||||
str/trim
|
|
||||||
(str/replace #"[^\w\s\-_()]+" "")
|
|
||||||
(str/replace #"\s+" " ")
|
|
||||||
str/trim)
|
|
||||||
""))
|
|
||||||
|
|
||||||
(defn get-initials
|
|
||||||
"Returns up to two uppercase initials extracted from a string.
|
|
||||||
Non-letter prefixes in each token are ignored."
|
|
||||||
[name]
|
|
||||||
(->> (str/split (str/trim (or name "")) #"\s+")
|
|
||||||
(keep #(first (re-seq #"[a-zA-Z]" %)))
|
|
||||||
(take 2)
|
|
||||||
(map str/upper)
|
|
||||||
(apply str)))
|
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; Util protocols
|
;; Util protocols
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|||||||
@ -1,115 +0,0 @@
|
|||||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
||||||
;;
|
|
||||||
;; Copyright (c) KALEIDOS INC
|
|
||||||
|
|
||||||
(ns app.common.files.comp-processors
|
|
||||||
"Repair, migration or transformation utilities for components."
|
|
||||||
(:require
|
|
||||||
[app.common.logging :as log]
|
|
||||||
[app.common.types.component :as ctk]
|
|
||||||
[app.common.types.file :as ctf]))
|
|
||||||
|
|
||||||
(log/set-level! :warn)
|
|
||||||
|
|
||||||
(defn remove-unneeded-objects-in-components
|
|
||||||
"Some components have an :objects attribute, despite not being deleted. This removes it.
|
|
||||||
It also adds an empty :objects if it's deleted and does not have it."
|
|
||||||
[file-data]
|
|
||||||
(ctf/update-components
|
|
||||||
file-data
|
|
||||||
(fn [component]
|
|
||||||
(if (:deleted component)
|
|
||||||
(if (nil? (:objects component))
|
|
||||||
(do
|
|
||||||
(log/warn :msg "Adding empty :objects to deleted component"
|
|
||||||
:component-id (:id component)
|
|
||||||
:component-name (:name component)
|
|
||||||
:file-id (:id file-data))
|
|
||||||
(assoc component :objects {}))
|
|
||||||
component)
|
|
||||||
(if (contains? component :objects)
|
|
||||||
(do
|
|
||||||
(log/warn :msg "Removing :objects from non-deleted component"
|
|
||||||
:component-id (:id component)
|
|
||||||
:component-name (:name component)
|
|
||||||
:file-id (:id file-data))
|
|
||||||
(dissoc component :objects))
|
|
||||||
component)))))
|
|
||||||
|
|
||||||
(defn fix-missing-swap-slots
|
|
||||||
"Locate shapes that have been swapped (i.e. their shape-ref does not point to the near match) but
|
|
||||||
they don't have a swap slot. In this case, add one pointing to the near match."
|
|
||||||
[file-data libraries]
|
|
||||||
(ctf/update-all-shapes
|
|
||||||
file-data
|
|
||||||
(fn [shape]
|
|
||||||
(if (ctk/subcopy-head? shape)
|
|
||||||
(let [container (:container (meta shape))
|
|
||||||
file {:id (:id file-data) :data file-data}
|
|
||||||
near-match (ctf/find-near-match file container libraries shape :include-deleted? true :with-context? false)]
|
|
||||||
(if (and (some? near-match)
|
|
||||||
(not= (:shape-ref shape) (:id near-match))
|
|
||||||
(nil? (ctk/get-swap-slot shape)))
|
|
||||||
(let [updated-shape (ctk/set-swap-slot shape (:id near-match))]
|
|
||||||
(log/warn :msg "Adding missing swap slot to shape"
|
|
||||||
:shape-id (:id shape)
|
|
||||||
:shape-name (:name shape)
|
|
||||||
:swap-slot (:id near-match)
|
|
||||||
:file-id (:id file)
|
|
||||||
:container-id (:id container)
|
|
||||||
:container-type (:type container))
|
|
||||||
{:result :update :updated-shape updated-shape})
|
|
||||||
{:result :keep}))
|
|
||||||
{:result :keep}))))
|
|
||||||
|
|
||||||
(defn sync-component-id-with-ref-shape
|
|
||||||
"Ensure that all copies heads have the same component id and file as the referenced shape.
|
|
||||||
There may be bugs that cause them to get out of sync."
|
|
||||||
[file-data libraries]
|
|
||||||
(letfn [(sync-one-iteration
|
|
||||||
[file-data libraries]
|
|
||||||
(ctf/update-all-shapes
|
|
||||||
file-data
|
|
||||||
(fn [shape]
|
|
||||||
(if (and (ctk/subcopy-head? shape) (nil? (ctk/get-swap-slot shape)))
|
|
||||||
(let [container (:container (meta shape))
|
|
||||||
file {:id (:id file-data) :data file-data}
|
|
||||||
ref-shape (ctf/find-ref-shape file container libraries shape {:include-deleted? true :with-context? true})]
|
|
||||||
(if (and (some? ref-shape)
|
|
||||||
(or (not= (:component-id shape) (:component-id ref-shape))
|
|
||||||
(not= (:component-file shape) (:component-file ref-shape))))
|
|
||||||
(let [shape' (cond-> shape
|
|
||||||
(some? (:component-id ref-shape))
|
|
||||||
(assoc :component-id (:component-id ref-shape))
|
|
||||||
|
|
||||||
(nil? (:component-id ref-shape))
|
|
||||||
(dissoc :component-id)
|
|
||||||
|
|
||||||
(some? (:component-file ref-shape))
|
|
||||||
(assoc :component-file (:component-file ref-shape))
|
|
||||||
|
|
||||||
(nil? (:component-file ref-shape))
|
|
||||||
(dissoc :component-file))]
|
|
||||||
(log/warn :msg "Syncing component id and file with ref shape"
|
|
||||||
:shape-id (:id shape)
|
|
||||||
:shape-name (:name shape)
|
|
||||||
:component-id (:component-id shape')
|
|
||||||
:component-file (:component-file shape')
|
|
||||||
:ref-shape-id (:id ref-shape)
|
|
||||||
:file-id (:id file)
|
|
||||||
:container-id (:id container)
|
|
||||||
:container-type (:type container))
|
|
||||||
{:result :update :updated-shape shape'})
|
|
||||||
{:result :keep}))
|
|
||||||
{:result :keep}))))]
|
|
||||||
;; If a copy inside a main is updated, we need to repeat the process for the change to be
|
|
||||||
;; propagated to all copies.
|
|
||||||
(loop [current-data file-data
|
|
||||||
iteration 0]
|
|
||||||
(let [next-data (sync-one-iteration current-data libraries)]
|
|
||||||
(if (or (= current-data next-data)
|
|
||||||
(> iteration 20)) ;; safety bound
|
|
||||||
next-data
|
|
||||||
(recur next-data (inc iteration)))))))
|
|
||||||
@ -10,7 +10,6 @@
|
|||||||
[app.common.data.macros :as dm]
|
[app.common.data.macros :as dm]
|
||||||
[app.common.features :as cfeat]
|
[app.common.features :as cfeat]
|
||||||
[app.common.files.changes :as cpc]
|
[app.common.files.changes :as cpc]
|
||||||
[app.common.files.comp-processors :as cfcp]
|
|
||||||
[app.common.files.defaults :as cfd]
|
[app.common.files.defaults :as cfd]
|
||||||
[app.common.files.helpers :as cfh]
|
[app.common.files.helpers :as cfh]
|
||||||
[app.common.geom.matrix :as gmt]
|
[app.common.geom.matrix :as gmt]
|
||||||
@ -1787,24 +1786,6 @@
|
|||||||
(update :pages-index d/update-vals update-container)
|
(update :pages-index d/update-vals update-container)
|
||||||
(d/update-when :components d/update-vals update-container))))
|
(d/update-when :components d/update-vals update-container))))
|
||||||
|
|
||||||
(defmethod migrate-data "0018-remove-unneeded-objects-from-components"
|
|
||||||
[data _]
|
|
||||||
(cfcp/remove-unneeded-objects-in-components data))
|
|
||||||
|
|
||||||
(defmethod migrate-data "0019-fix-missing-swap-slots"
|
|
||||||
[data _]
|
|
||||||
(let [libraries (if (:libs data)
|
|
||||||
(deref (:libs data))
|
|
||||||
{})]
|
|
||||||
(cfcp/fix-missing-swap-slots data libraries)))
|
|
||||||
|
|
||||||
(defmethod migrate-data "0020-sync-component-id-with-near-main"
|
|
||||||
[data _]
|
|
||||||
(let [libraries (if (:libs data)
|
|
||||||
(deref (:libs data))
|
|
||||||
{})]
|
|
||||||
(cfcp/sync-component-id-with-ref-shape data libraries)))
|
|
||||||
|
|
||||||
(def available-migrations
|
(def available-migrations
|
||||||
(into (d/ordered-set)
|
(into (d/ordered-set)
|
||||||
["legacy-2"
|
["legacy-2"
|
||||||
@ -1879,7 +1860,4 @@
|
|||||||
"0015-fix-text-attrs-blank-strings"
|
"0015-fix-text-attrs-blank-strings"
|
||||||
"0015-clean-shadow-color"
|
"0015-clean-shadow-color"
|
||||||
"0016-copy-fills-from-position-data-to-text-node"
|
"0016-copy-fills-from-position-data-to-text-node"
|
||||||
"0017-fix-layout-flex-dir"
|
"0017-fix-layout-flex-dir"]))
|
||||||
"0018-remove-unneeded-objects-from-components"
|
|
||||||
"0019-fix-missing-swap-slots"
|
|
||||||
"0020-sync-component-id-with-near-main"]))
|
|
||||||
|
|||||||
@ -334,31 +334,6 @@
|
|||||||
(pcb/with-file-data file-data)
|
(pcb/with-file-data file-data)
|
||||||
(pcb/update-shapes [(:id shape)] repair-shape))))
|
(pcb/update-shapes [(:id shape)] repair-shape))))
|
||||||
|
|
||||||
(defmethod repair-error :component-id-mismatch
|
|
||||||
[_ {:keys [shape page-id args] :as error} file-data _]
|
|
||||||
(let [repair-shape
|
|
||||||
(fn [shape]
|
|
||||||
; Set the component-id and component-file to the ones of the near main
|
|
||||||
(log/debug :hint (str " -> set component-id to " (:component-id args)))
|
|
||||||
(log/debug :hint (str " -> set component-file to " (:component-file args)))
|
|
||||||
(cond-> shape
|
|
||||||
(some? (:component-id args))
|
|
||||||
(assoc :component-id (:component-id args))
|
|
||||||
|
|
||||||
(nil? (:component-id args))
|
|
||||||
(dissoc :component-id)
|
|
||||||
|
|
||||||
(some? (:component-file args))
|
|
||||||
(assoc :component-file (:component-file args))
|
|
||||||
|
|
||||||
(nil? (:component-file args))
|
|
||||||
(dissoc :component-file)))]
|
|
||||||
|
|
||||||
(log/dbg :hint "repairing shape :component-id-mismatch" :id (:id shape) :name (:name shape) :page-id page-id)
|
|
||||||
(-> (pcb/empty-changes nil page-id)
|
|
||||||
(pcb/with-file-data file-data)
|
|
||||||
(pcb/update-shapes [(:id shape)] repair-shape))))
|
|
||||||
|
|
||||||
(defmethod repair-error :ref-shape-is-head
|
(defmethod repair-error :ref-shape-is-head
|
||||||
[_ {:keys [shape page-id args] :as error} file-data _]
|
[_ {:keys [shape page-id args] :as error} file-data _]
|
||||||
(let [repair-shape
|
(let [repair-shape
|
||||||
@ -526,7 +501,7 @@
|
|||||||
(pcb/update-shapes [(:id shape)] repair-shape))))
|
(pcb/update-shapes [(:id shape)] repair-shape))))
|
||||||
|
|
||||||
(defmethod repair-error :component-nil-objects-not-allowed
|
(defmethod repair-error :component-nil-objects-not-allowed
|
||||||
[_ {component :shape} file-data _] ; in this error the :shape argument is the component
|
[_ {:keys [shape] :as error} file-data _]
|
||||||
(let [repair-component
|
(let [repair-component
|
||||||
(fn [component]
|
(fn [component]
|
||||||
;; Remove the objects key, or set it to {} if the component is deleted
|
;; Remove the objects key, or set it to {} if the component is deleted
|
||||||
@ -538,26 +513,10 @@
|
|||||||
(log/debug :hint " -> remove :objects")
|
(log/debug :hint " -> remove :objects")
|
||||||
(dissoc component :objects))))]
|
(dissoc component :objects))))]
|
||||||
|
|
||||||
(log/dbg :hint "repairing component :component-nil-objects-not-allowed" :id (:id component) :name (:name component))
|
(log/dbg :hint "repairing component :component-nil-objects-not-allowed" :id (:id shape) :name (:name shape))
|
||||||
(-> (pcb/empty-changes nil)
|
(-> (pcb/empty-changes nil)
|
||||||
(pcb/with-library-data file-data)
|
(pcb/with-library-data file-data)
|
||||||
(pcb/update-component (:id component) repair-component))))
|
(pcb/update-component (:id shape) repair-component))))
|
||||||
|
|
||||||
(defmethod repair-error :non-deleted-component-cannot-have-objects
|
|
||||||
[_ {component :shape} file-data _] ; in this error the :shape argument is the component
|
|
||||||
(let [repair-component
|
|
||||||
(fn [component]
|
|
||||||
; Remove the :objects field
|
|
||||||
(if-not (:deleted component)
|
|
||||||
(do
|
|
||||||
(log/debug :hint " -> remove :objects")
|
|
||||||
(dissoc component :objects))
|
|
||||||
component))]
|
|
||||||
|
|
||||||
(log/dbg :hint "repairing component :non-deleted-component-cannot-have-objects" :id (:id component) :name (:name component))
|
|
||||||
(-> (pcb/empty-changes nil)
|
|
||||||
(pcb/with-library-data file-data)
|
|
||||||
(pcb/update-component (:id component) repair-component))))
|
|
||||||
|
|
||||||
(defmethod repair-error :invalid-text-touched
|
(defmethod repair-error :invalid-text-touched
|
||||||
[_ {:keys [shape page-id] :as error} file-data _]
|
[_ {:keys [shape page-id] :as error} file-data _]
|
||||||
|
|||||||
@ -1,74 +0,0 @@
|
|||||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
||||||
;;
|
|
||||||
;; Copyright (c) KALEIDOS INC
|
|
||||||
|
|
||||||
(ns app.common.files.stats
|
|
||||||
"Pure helpers that compute aggregate statistics for a file data map.
|
|
||||||
|
|
||||||
Given a decoded file data structure (the value stored under `:data`
|
|
||||||
on a file row), produces a small map with page/shape/library counts.
|
|
||||||
Intended to be cheap — a single pass over each page's `:objects`
|
|
||||||
map, no database access, no side effects."
|
|
||||||
(:require
|
|
||||||
[app.common.uuid :as uuid]))
|
|
||||||
|
|
||||||
(def empty-shape-counts
|
|
||||||
{:total 0 :by-type {}})
|
|
||||||
|
|
||||||
(defn- inc-type
|
|
||||||
[by-type shape-type]
|
|
||||||
(if (nil? shape-type)
|
|
||||||
by-type
|
|
||||||
(update by-type shape-type (fnil inc 0))))
|
|
||||||
|
|
||||||
(defn count-shapes-by-type
|
|
||||||
"Walk an `:objects` map of a single page and return
|
|
||||||
`{:total N :by-type {:rect N :frame N ...}}`. The synthetic root
|
|
||||||
shape at `uuid/zero` is skipped so it never contributes to totals."
|
|
||||||
[objects]
|
|
||||||
(if (empty? objects)
|
|
||||||
empty-shape-counts
|
|
||||||
(reduce-kv
|
|
||||||
(fn [acc id shape]
|
|
||||||
(if (= id uuid/zero)
|
|
||||||
acc
|
|
||||||
(-> acc
|
|
||||||
(update :total inc)
|
|
||||||
(update :by-type inc-type (:type shape)))))
|
|
||||||
empty-shape-counts
|
|
||||||
objects)))
|
|
||||||
|
|
||||||
(defn- merge-shape-counts
|
|
||||||
[a b]
|
|
||||||
{:total (+ (:total a) (:total b))
|
|
||||||
:by-type (merge-with + (:by-type a) (:by-type b))})
|
|
||||||
|
|
||||||
(defn- aggregate-shape-counts
|
|
||||||
[pages-index]
|
|
||||||
(transduce
|
|
||||||
(map (comp count-shapes-by-type :objects))
|
|
||||||
(completing merge-shape-counts)
|
|
||||||
empty-shape-counts
|
|
||||||
(vals pages-index)))
|
|
||||||
|
|
||||||
(defn calc-file-stats
|
|
||||||
"Given a decoded file data map with the standard keys
|
|
||||||
`:pages-index`, `:components`, `:deleted-components`, `:colors`
|
|
||||||
and `:typographies`, return per-file aggregates.
|
|
||||||
|
|
||||||
The result is a plain map suitable for serialization; it never
|
|
||||||
contains any pointer-map or objects-map instances."
|
|
||||||
[fdata]
|
|
||||||
(let [pages-index (get fdata :pages-index)
|
|
||||||
components (get fdata :components)
|
|
||||||
deleted-components (get fdata :deleted-components)
|
|
||||||
colors (get fdata :colors)
|
|
||||||
typographies (get fdata :typographies)]
|
|
||||||
{:page-count (count pages-index)
|
|
||||||
:shape-counts (aggregate-shape-counts pages-index)
|
|
||||||
:component-count (count components)
|
|
||||||
:deleted-component-count (count deleted-components)
|
|
||||||
:color-count (count colors)
|
|
||||||
:typography-count (count typographies)}))
|
|
||||||
@ -147,27 +147,6 @@
|
|||||||
#(and (some? tokens-tree)
|
#(and (some? tokens-tree)
|
||||||
(not (ctob/token-name-path-exists? % tokens-tree)))]])
|
(not (ctob/token-name-path-exists? % tokens-tree)))]])
|
||||||
|
|
||||||
(defn make-node-token-name-schema
|
|
||||||
"Dynamically generates a schema to check a token node name, adding translated error messages
|
|
||||||
and two additional validations:
|
|
||||||
- Min and max length.
|
|
||||||
- Checks if other token with a path derived from the name already exists at `tokens-tree`.
|
|
||||||
e.g. it's not allowed to create a token `foo.bar` if a token `foo` already exists."
|
|
||||||
[active-tokens tokens-tree node]
|
|
||||||
[:and
|
|
||||||
[:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
|
|
||||||
(-> cto/schema:token-node-name
|
|
||||||
(sm/update-properties assoc :error/fn #(str (:value %) (tr "workspace.tokens.token-name-validation-error"))))
|
|
||||||
[:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))}
|
|
||||||
(fn [name]
|
|
||||||
(let [current-path (:path node)
|
|
||||||
current-name (:name node)
|
|
||||||
new-tokens (ctob/update-tokens-group active-tokens current-path current-name name)]
|
|
||||||
(and (some? new-tokens)
|
|
||||||
(some (fn [[token-name _]]
|
|
||||||
(not (ctob/token-name-path-exists? token-name tokens-tree)))
|
|
||||||
new-tokens))))]])
|
|
||||||
|
|
||||||
(def schema:token-description
|
(def schema:token-description
|
||||||
[:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}])
|
[:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}])
|
||||||
|
|
||||||
@ -186,11 +165,6 @@
|
|||||||
(when (and name value)
|
(when (and name value)
|
||||||
(not (cto/token-value-self-reference? name value))))]])
|
(not (cto/token-value-self-reference? name value))))]])
|
||||||
|
|
||||||
(defn make-node-token-schema
|
|
||||||
[active-tokens tokens-tree node]
|
|
||||||
[:map
|
|
||||||
[:name (make-node-token-name-schema active-tokens tokens-tree node)]])
|
|
||||||
|
|
||||||
(defn convert-dtcg-token
|
(defn convert-dtcg-token
|
||||||
"Convert token attributes as they come from a decoded json, with DTCG types, to internal types.
|
"Convert token attributes as they come from a decoded json, with DTCG types, to internal types.
|
||||||
Eg. From this:
|
Eg. From this:
|
||||||
@ -314,12 +288,16 @@
|
|||||||
{:value parsed-value
|
{:value parsed-value
|
||||||
:unit unit}))))
|
:unit unit}))))
|
||||||
|
|
||||||
|
;; FIXME: looks very redundant function
|
||||||
|
(defn token-identifier
|
||||||
|
[{:keys [name] :as _token}]
|
||||||
|
name)
|
||||||
|
|
||||||
(defn attributes-map
|
(defn attributes-map
|
||||||
"Creates an attributes map using collection of `attributes` for `id`."
|
"Creats an attributes map using collection of `attributes` for `id`."
|
||||||
[attributes token]
|
[attributes token]
|
||||||
(into {}
|
(->> (map (fn [attr] [attr (token-identifier token)]) attributes)
|
||||||
(map (fn [attr] [attr (:name token)]))
|
(into {})))
|
||||||
attributes))
|
|
||||||
|
|
||||||
(defn remove-attributes-for-token
|
(defn remove-attributes-for-token
|
||||||
"Removes applied tokens with `token-name` for the given `attributes` set from `applied-tokens`."
|
"Removes applied tokens with `token-name` for the given `attributes` set from `applied-tokens`."
|
||||||
@ -335,7 +313,7 @@
|
|||||||
"Test if `token` is applied to a `shape` on single `token-attribute`."
|
"Test if `token` is applied to a `shape` on single `token-attribute`."
|
||||||
[token shape token-attribute]
|
[token shape token-attribute]
|
||||||
(when-let [id (dm/get-in shape [:applied-tokens token-attribute])]
|
(when-let [id (dm/get-in shape [:applied-tokens token-attribute])]
|
||||||
(= (:name token) id)))
|
(= (token-identifier token) id)))
|
||||||
|
|
||||||
(defn token-applied?
|
(defn token-applied?
|
||||||
"Test if `token` is applied to a `shape` with at least one of the given `token-attributes`."
|
"Test if `token` is applied to a `shape` with at least one of the given `token-attributes`."
|
||||||
|
|||||||
@ -51,7 +51,6 @@
|
|||||||
:ref-shape-is-head
|
:ref-shape-is-head
|
||||||
:ref-shape-is-not-head
|
:ref-shape-is-not-head
|
||||||
:shape-ref-in-main
|
:shape-ref-in-main
|
||||||
:component-id-mismatch
|
|
||||||
:root-main-not-allowed
|
:root-main-not-allowed
|
||||||
:nested-main-not-allowed
|
:nested-main-not-allowed
|
||||||
:root-copy-not-allowed
|
:root-copy-not-allowed
|
||||||
@ -60,7 +59,6 @@
|
|||||||
:not-head-copy-not-allowed
|
:not-head-copy-not-allowed
|
||||||
:not-component-not-allowed
|
:not-component-not-allowed
|
||||||
:component-nil-objects-not-allowed
|
:component-nil-objects-not-allowed
|
||||||
:non-deleted-component-cannot-have-objects
|
|
||||||
:instance-head-not-frame
|
:instance-head-not-frame
|
||||||
:invalid-text-touched
|
:invalid-text-touched
|
||||||
:misplaced-slot
|
:misplaced-slot
|
||||||
@ -328,20 +326,6 @@
|
|||||||
:component-file (:component-file ref-shape)
|
:component-file (:component-file ref-shape)
|
||||||
:component-id (:component-id ref-shape)))))
|
:component-id (:component-id ref-shape)))))
|
||||||
|
|
||||||
(defn- check-ref-component-id
|
|
||||||
"Validate that if the copy has not been swapped, the component-id and component-file are
|
|
||||||
the same as in the referenced shape in the near main."
|
|
||||||
[shape file page libraries]
|
|
||||||
(when (nil? (ctk/get-swap-slot shape))
|
|
||||||
(when-let [ref-shape (ctf/find-ref-shape file page libraries shape :include-deleted? true)]
|
|
||||||
(when (or (not= (:component-id shape) (:component-id ref-shape))
|
|
||||||
(not= (:component-file shape) (:component-file ref-shape)))
|
|
||||||
(report-error :component-id-mismatch
|
|
||||||
"Nested copy component-id and component-file must be the same as the near main"
|
|
||||||
shape file page
|
|
||||||
:component-id (:component-id ref-shape)
|
|
||||||
:component-file (:component-file ref-shape))))))
|
|
||||||
|
|
||||||
(defn- check-empty-swap-slot
|
(defn- check-empty-swap-slot
|
||||||
"Validate that this shape does not have any swap slot."
|
"Validate that this shape does not have any swap slot."
|
||||||
[shape file page]
|
[shape file page]
|
||||||
@ -366,19 +350,6 @@
|
|||||||
"This shape has children with the same swap slot"
|
"This shape has children with the same swap slot"
|
||||||
shape file page)))
|
shape file page)))
|
||||||
|
|
||||||
(defn- check-required-swap-slot
|
|
||||||
"Validate that the shape has swap-slot if it's a subinstance head and the ref shape is not the
|
|
||||||
matching shape by position in the near main."
|
|
||||||
[shape file page libraries]
|
|
||||||
(let [near-match (ctf/find-near-match file page libraries shape :include-deleted? true :with-context? false)]
|
|
||||||
(when (and (some? near-match)
|
|
||||||
(not= (:shape-ref shape) (:id near-match))
|
|
||||||
(nil? (ctk/get-swap-slot shape)))
|
|
||||||
(report-error :missing-slot
|
|
||||||
"Shape has been swapped, should have swap slot"
|
|
||||||
shape file page
|
|
||||||
:swap-slot (or (ctk/get-swap-slot near-match) (:id near-match))))))
|
|
||||||
|
|
||||||
(defn- check-valid-touched
|
(defn- check-valid-touched
|
||||||
"Validate that the text touched flags are coherent."
|
"Validate that the text touched flags are coherent."
|
||||||
[shape file page]
|
[shape file page]
|
||||||
@ -447,8 +418,6 @@
|
|||||||
(check-component-not-main-head shape file page libraries)
|
(check-component-not-main-head shape file page libraries)
|
||||||
(check-component-not-root shape file page)
|
(check-component-not-root shape file page)
|
||||||
(check-valid-touched shape file page)
|
(check-valid-touched shape file page)
|
||||||
(check-ref-component-id shape file page libraries)
|
|
||||||
(check-required-swap-slot shape file page libraries)
|
|
||||||
;; We can have situations where the nested copy and the ancestor copy come from different libraries and some of them have been dettached
|
;; We can have situations where the nested copy and the ancestor copy come from different libraries and some of them have been dettached
|
||||||
;; so we only validate the shape-ref if the ancestor is from a valid library
|
;; so we only validate the shape-ref if the ancestor is from a valid library
|
||||||
(when library-exists
|
(when library-exists
|
||||||
@ -489,7 +458,8 @@
|
|||||||
(defn- check-variant-container
|
(defn- check-variant-container
|
||||||
"Shape is a variant container, so:
|
"Shape is a variant container, so:
|
||||||
-all its children should be variants with variant-id equals to the shape-id
|
-all its children should be variants with variant-id equals to the shape-id
|
||||||
-all the components should have the same properties"
|
-all the components should have the same properties
|
||||||
|
"
|
||||||
[shape file page]
|
[shape file page]
|
||||||
(let [shape-id (:id shape)
|
(let [shape-id (:id shape)
|
||||||
shapes (:shapes shape)
|
shapes (:shapes shape)
|
||||||
@ -678,13 +648,6 @@
|
|||||||
"Component main not allowed inside other component"
|
"Component main not allowed inside other component"
|
||||||
main-instance file component-page))))
|
main-instance file component-page))))
|
||||||
|
|
||||||
(defn- check-not-objects
|
|
||||||
[component file]
|
|
||||||
(when (d/not-empty? (:objects component))
|
|
||||||
(report-error :non-deleted-component-cannot-have-objects
|
|
||||||
"A non-deleted component cannot have shapes inside"
|
|
||||||
component file nil)))
|
|
||||||
|
|
||||||
(defn- check-component
|
(defn- check-component
|
||||||
"Validate semantic coherence of a component. Report all errors found."
|
"Validate semantic coherence of a component. Report all errors found."
|
||||||
[component file]
|
[component file]
|
||||||
@ -693,8 +656,7 @@
|
|||||||
"Objects list cannot be nil"
|
"Objects list cannot be nil"
|
||||||
component file nil))
|
component file nil))
|
||||||
(when-not (:deleted component)
|
(when-not (:deleted component)
|
||||||
(check-main-inside-main component file)
|
(check-main-inside-main component file))
|
||||||
(check-not-objects component file))
|
|
||||||
(when (:deleted component)
|
(when (:deleted component)
|
||||||
(check-component-duplicate-swap-slot component file)
|
(check-component-duplicate-swap-slot component file)
|
||||||
(check-ref-cycles component file))
|
(check-ref-cycles component file))
|
||||||
@ -712,6 +674,8 @@
|
|||||||
;; PUBLIC API: VALIDATION FUNCTIONS
|
;; PUBLIC API: VALIDATION FUNCTIONS
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
|
(declare check-swap-slots)
|
||||||
|
|
||||||
(defn validate-file
|
(defn validate-file
|
||||||
"Validate full referential integrity and semantic coherence on file data.
|
"Validate full referential integrity and semantic coherence on file data.
|
||||||
|
|
||||||
@ -722,6 +686,8 @@
|
|||||||
|
|
||||||
(doseq [page (filter :id (ctpl/pages-seq data))]
|
(doseq [page (filter :id (ctpl/pages-seq data))]
|
||||||
(check-shape uuid/zero file page libraries)
|
(check-shape uuid/zero file page libraries)
|
||||||
|
(when (str/includes? (:name file) "check-swap-slot")
|
||||||
|
(check-swap-slots uuid/zero file page libraries))
|
||||||
(->> (get-orphan-shapes page)
|
(->> (get-orphan-shapes page)
|
||||||
(run! #(check-shape % file page libraries))))
|
(run! #(check-shape % file page libraries))))
|
||||||
|
|
||||||
@ -762,3 +728,40 @@
|
|||||||
:hint "error on validating file referential integrity"
|
:hint "error on validating file referential integrity"
|
||||||
:file-id (:id file)
|
:file-id (:id file)
|
||||||
:details errors)))
|
:details errors)))
|
||||||
|
|
||||||
|
(declare compare-slots)
|
||||||
|
|
||||||
|
;; Optional check to look for missing swap slots.
|
||||||
|
;; Search for copies that do not point the shape-ref to the near component but don't have swap slot
|
||||||
|
;; (looking for position relative to the parent, in the copy and the main).
|
||||||
|
;;
|
||||||
|
;; This check cannot be generally enabled, because files that have been migrated from components v1
|
||||||
|
;; may have copies with shapes that do not match by position, but have not been swapped. So we enable
|
||||||
|
;; it for specific files only. To activate the check, you need to add the string "check-swap-slot" to
|
||||||
|
;; the name of the file.
|
||||||
|
(defn- check-swap-slots
|
||||||
|
[shape-id file page libraries]
|
||||||
|
(let [shape (ctst/get-shape page shape-id)]
|
||||||
|
(if (and (ctk/instance-root? shape) (ctk/in-component-copy? shape))
|
||||||
|
(let [ref-shape (ctf/find-ref-shape file page libraries shape :include-deleted? true :with-context? true)
|
||||||
|
container (:container (meta ref-shape))]
|
||||||
|
(when (some? ref-shape)
|
||||||
|
(compare-slots shape ref-shape file page container)))
|
||||||
|
(doall (for [child-id (:shapes shape)]
|
||||||
|
(check-swap-slots child-id file page libraries))))))
|
||||||
|
|
||||||
|
(defn- compare-slots
|
||||||
|
[shape-copy shape-main file container-copy container-main]
|
||||||
|
(if (and (not= (:shape-ref shape-copy) (:id shape-main))
|
||||||
|
(nil? (ctk/get-swap-slot shape-copy)))
|
||||||
|
(report-error :missing-slot
|
||||||
|
"Shape has been swapped, should have swap slot"
|
||||||
|
shape-copy file container-copy
|
||||||
|
:swap-slot (or (ctk/get-swap-slot shape-main) (:id shape-main)))
|
||||||
|
(when (nil? (ctk/get-swap-slot shape-copy))
|
||||||
|
(let [children-id-pairs (d/zip-all (:shapes shape-copy) (:shapes shape-main))]
|
||||||
|
(doall (for [[child-copy-id child-main-id] children-id-pairs]
|
||||||
|
(let [child-copy (ctst/get-shape container-copy child-copy-id)
|
||||||
|
child-main (ctst/get-shape container-main child-main-id)]
|
||||||
|
(when (and (some? child-copy) (some? child-main))
|
||||||
|
(compare-slots child-copy child-main file container-copy container-main)))))))))
|
||||||
|
|||||||
@ -128,8 +128,6 @@
|
|||||||
:token-shadow
|
:token-shadow
|
||||||
:token-tokenscript
|
:token-tokenscript
|
||||||
:token-import-from-library
|
:token-import-from-library
|
||||||
:token-typography-row
|
|
||||||
|
|
||||||
;; Only for developtment.
|
;; Only for developtment.
|
||||||
:transit-readable-response
|
:transit-readable-response
|
||||||
:user-feedback
|
:user-feedback
|
||||||
@ -196,7 +194,8 @@
|
|||||||
:enable-inspect-styles
|
:enable-inspect-styles
|
||||||
:enable-feature-fdata-objects-map
|
:enable-feature-fdata-objects-map
|
||||||
:enable-feature-render-wasm
|
:enable-feature-render-wasm
|
||||||
:enable-token-import-from-library])
|
;; Temporary deactivated
|
||||||
|
#_:enable-token-import-from-library])
|
||||||
|
|
||||||
(defn parse
|
(defn parse
|
||||||
[& flags]
|
[& flags]
|
||||||
|
|||||||
@ -17,11 +17,11 @@
|
|||||||
java.util.List
|
java.util.List
|
||||||
linked.map.LinkedMap
|
linked.map.LinkedMap
|
||||||
linked.set.LinkedSet
|
linked.set.LinkedSet
|
||||||
org.fressian.handlers.ReadHandler
|
|
||||||
org.fressian.handlers.WriteHandler
|
|
||||||
org.fressian.Reader
|
org.fressian.Reader
|
||||||
org.fressian.StreamingWriter
|
org.fressian.StreamingWriter
|
||||||
org.fressian.Writer))
|
org.fressian.Writer
|
||||||
|
org.fressian.handlers.ReadHandler
|
||||||
|
org.fressian.handlers.WriteHandler))
|
||||||
|
|
||||||
(set! *warn-on-reflection* true)
|
(set! *warn-on-reflection* true)
|
||||||
|
|
||||||
|
|||||||
@ -8,11 +8,11 @@
|
|||||||
(:refer-clojure :exclude [get])
|
(:refer-clojure :exclude [get])
|
||||||
(:import
|
(:import
|
||||||
java.lang.AutoCloseable
|
java.lang.AutoCloseable
|
||||||
org.apache.commons.pool2.impl.DefaultPooledObject
|
|
||||||
org.apache.commons.pool2.impl.SoftReferenceObjectPool
|
|
||||||
org.apache.commons.pool2.ObjectPool
|
org.apache.commons.pool2.ObjectPool
|
||||||
org.apache.commons.pool2.PooledObject
|
org.apache.commons.pool2.PooledObject
|
||||||
org.apache.commons.pool2.PooledObjectFactory))
|
org.apache.commons.pool2.PooledObjectFactory
|
||||||
|
org.apache.commons.pool2.impl.DefaultPooledObject
|
||||||
|
org.apache.commons.pool2.impl.SoftReferenceObjectPool))
|
||||||
|
|
||||||
(defn pool?
|
(defn pool?
|
||||||
[o]
|
[o]
|
||||||
|
|||||||
@ -333,7 +333,7 @@
|
|||||||
(pcb/update-shapes [shape-id] #(do (log/trace :msg " -> promote to root")
|
(pcb/update-shapes [shape-id] #(do (log/trace :msg " -> promote to root")
|
||||||
(assoc % :component-root true)))
|
(assoc % :component-root true)))
|
||||||
|
|
||||||
(some? (ctk/get-swap-slot shape))
|
:always
|
||||||
; First level subinstances of a detached component can't have swap-slot
|
; First level subinstances of a detached component can't have swap-slot
|
||||||
(pcb/update-shapes [shape-id] #(do (log/trace :msg " -> remove swap-slot")
|
(pcb/update-shapes [shape-id] #(do (log/trace :msg " -> remove swap-slot")
|
||||||
(ctk/remove-swap-slot %)))
|
(ctk/remove-swap-slot %)))
|
||||||
@ -364,7 +364,7 @@
|
|||||||
(let [ref-shape (ctf/find-ref-shape file container libraries shape {:include-deleted? true})]
|
(let [ref-shape (ctf/find-ref-shape file container libraries shape {:include-deleted? true})]
|
||||||
(cond-> changes
|
(cond-> changes
|
||||||
(some? (:shape-ref ref-shape))
|
(some? (:shape-ref ref-shape))
|
||||||
(pcb/update-shapes [(:id shape)] #(do (log/trace :msg (str " (advanced to " (:shape-ref ref-shape) ")"))
|
(pcb/update-shapes [(:id shape)] #(do (log/trace :msg " (advanced)")
|
||||||
(assoc % :shape-ref (:shape-ref ref-shape))))
|
(assoc % :shape-ref (:shape-ref ref-shape))))
|
||||||
|
|
||||||
;; When advancing level, the normal touched groups (not swap slots) of the
|
;; When advancing level, the normal touched groups (not swap slots) of the
|
||||||
@ -374,18 +374,16 @@
|
|||||||
(pcb/update-shapes
|
(pcb/update-shapes
|
||||||
[(:id shape)]
|
[(:id shape)]
|
||||||
#(do (log/trace :msg " (merge touched)")
|
#(do (log/trace :msg " (merge touched)")
|
||||||
(log/trace :msg (str " (ref-shape: " (:id ref-shape) ")"))
|
|
||||||
(log/trace :msg (str " (ref touched: " (:touched ref-shape) ")"))
|
|
||||||
(assoc % :touched
|
(assoc % :touched
|
||||||
(set/union (:touched shape)
|
(clojure.set/union (:touched shape)
|
||||||
(ctk/normal-touched-groups ref-shape)))))
|
(ctk/normal-touched-groups ref-shape)))))
|
||||||
|
|
||||||
;; Swap slot must also be copied if the current shape has not any,
|
;; Swap slot must also be copied if the current shape has not any,
|
||||||
;; except if this is the first level subcopy.
|
;; except if this is the first level subcopy.
|
||||||
(and (some? (ctk/get-swap-slot ref-shape))
|
(and (some? (ctk/get-swap-slot ref-shape))
|
||||||
(nil? (ctk/get-swap-slot shape))
|
(nil? (ctk/get-swap-slot shape))
|
||||||
(not= (:id shape) shape-id))
|
(not= (:id shape) shape-id))
|
||||||
(pcb/update-shapes [(:id shape)] #(do (log/trace :msg (str " (got swap-slot " (ctk/get-swap-slot ref-shape) ")"))
|
(pcb/update-shapes [(:id shape)] #(do (log/trace :msg " (got swap-slot)")
|
||||||
(ctk/set-swap-slot % (ctk/get-swap-slot ref-shape))))
|
(ctk/set-swap-slot % (ctk/get-swap-slot ref-shape))))
|
||||||
|
|
||||||
;; If we can't get the ref-shape (e.g. it's in an external library not linked),
|
;; If we can't get the ref-shape (e.g. it's in an external library not linked),
|
||||||
@ -773,6 +771,14 @@
|
|||||||
;; is different than the one in the near component (Shape-2-2-1)
|
;; is different than the one in the near component (Shape-2-2-1)
|
||||||
;; but it's not touched.
|
;; but it's not touched.
|
||||||
|
|
||||||
|
(defn- redirect-shaperef ;;Set the :shape-ref of a shape pointing to the :id of its remote-shape
|
||||||
|
([container libraries shape]
|
||||||
|
(redirect-shaperef nil nil shape (ctf/find-remote-shape container libraries shape)))
|
||||||
|
([_ _ shape remote-shape]
|
||||||
|
(if (some? (:shape-ref shape))
|
||||||
|
(assoc shape :shape-ref (:id remote-shape))
|
||||||
|
shape)))
|
||||||
|
|
||||||
(defn generate-sync-shape-direct
|
(defn generate-sync-shape-direct
|
||||||
"Generate changes to synchronize one shape that is the root of a component
|
"Generate changes to synchronize one shape that is the root of a component
|
||||||
instance, and all its children, from the given component."
|
instance, and all its children, from the given component."
|
||||||
@ -784,12 +790,18 @@
|
|||||||
component (ctkl/get-component library (:component-id shape-inst) true)]
|
component (ctkl/get-component library (:component-id shape-inst) true)]
|
||||||
(if (and (ctk/in-component-copy? shape-inst)
|
(if (and (ctk/in-component-copy? shape-inst)
|
||||||
(or (ctf/direct-copy? shape-inst component container nil libraries) reset?)) ; In a normal sync, we don't want to sync remote mains, only direct/near
|
(or (ctf/direct-copy? shape-inst component container nil libraries) reset?)) ; In a normal sync, we don't want to sync remote mains, only direct/near
|
||||||
(let [shape-main (when component
|
(let [redirect-shaperef (partial redirect-shaperef container libraries)
|
||||||
|
|
||||||
|
shape-main (when component
|
||||||
(if reset?
|
(if reset?
|
||||||
;; the reset is against the ref-shape, not against the original shape of the component
|
;; the reset is against the ref-shape, not against the original shape of the component
|
||||||
(ctf/find-ref-shape file container libraries shape-inst)
|
(ctf/find-ref-shape file container libraries shape-inst)
|
||||||
(ctf/get-ref-shape library component shape-inst)))
|
(ctf/get-ref-shape library component shape-inst)))
|
||||||
|
|
||||||
|
shape-inst (if reset?
|
||||||
|
(redirect-shaperef shape-inst shape-main)
|
||||||
|
shape-inst)
|
||||||
|
|
||||||
initial-root? (:component-root shape-inst)
|
initial-root? (:component-root shape-inst)
|
||||||
|
|
||||||
root-inst shape-inst
|
root-inst shape-inst
|
||||||
@ -807,8 +819,8 @@
|
|||||||
root-inst
|
root-inst
|
||||||
root-main
|
root-main
|
||||||
reset?
|
reset?
|
||||||
initial-root?)
|
initial-root?
|
||||||
|
redirect-shaperef)
|
||||||
;; If the component is not found, because the master component has been
|
;; If the component is not found, because the master component has been
|
||||||
;; deleted or the library unlinked, do nothing.
|
;; deleted or the library unlinked, do nothing.
|
||||||
changes))
|
changes))
|
||||||
@ -832,7 +844,7 @@
|
|||||||
nil))))))
|
nil))))))
|
||||||
|
|
||||||
(defn- generate-sync-shape-direct-recursive
|
(defn- generate-sync-shape-direct-recursive
|
||||||
[changes container shape-inst component library file libraries shape-main root-inst root-main reset? initial-root?]
|
[changes container shape-inst component library file libraries shape-main root-inst root-main reset? initial-root? redirect-shaperef]
|
||||||
(shape-log :debug (:id shape-inst) container
|
(shape-log :debug (:id shape-inst) container
|
||||||
:msg "Sync shape direct recursive"
|
:msg "Sync shape direct recursive"
|
||||||
:shape-inst (str (:name shape-inst) " " (pretty-uuid (:id shape-inst)))
|
:shape-inst (str (:name shape-inst) " " (pretty-uuid (:id shape-inst)))
|
||||||
@ -879,6 +891,9 @@
|
|||||||
children-inst (vec (ctn/get-direct-children container shape-inst))
|
children-inst (vec (ctn/get-direct-children container shape-inst))
|
||||||
children-main (vec (ctn/get-direct-children component-container shape-main))
|
children-main (vec (ctn/get-direct-children component-container shape-main))
|
||||||
|
|
||||||
|
children-inst (if reset?
|
||||||
|
(map #(redirect-shaperef %) children-inst) children-inst)
|
||||||
|
|
||||||
only-inst (fn [changes child-inst]
|
only-inst (fn [changes child-inst]
|
||||||
(shape-log :trace (:id child-inst) container
|
(shape-log :trace (:id child-inst) container
|
||||||
:msg "Only inst"
|
:msg "Only inst"
|
||||||
@ -927,7 +942,8 @@
|
|||||||
root-inst
|
root-inst
|
||||||
root-main
|
root-main
|
||||||
reset?
|
reset?
|
||||||
initial-root?))
|
initial-root?
|
||||||
|
redirect-shaperef))
|
||||||
|
|
||||||
swapped (fn [changes child-inst child-main]
|
swapped (fn [changes child-inst child-main]
|
||||||
(shape-log :trace (:id child-inst) container
|
(shape-log :trace (:id child-inst) container
|
||||||
@ -992,13 +1008,16 @@
|
|||||||
the values in the shape and all its children."
|
the values in the shape and all its children."
|
||||||
[changes file libraries container shape-id]
|
[changes file libraries container shape-id]
|
||||||
(shape-log :debug shape-id container :msg "Sync shape inverse" :shape (str shape-id))
|
(shape-log :debug shape-id container :msg "Sync shape inverse" :shape (str shape-id))
|
||||||
(let [shape-inst (ctn/get-shape container shape-id)
|
(let [redirect-shaperef (partial redirect-shaperef container libraries)
|
||||||
|
shape-inst (ctn/get-shape container shape-id)
|
||||||
library (dm/get-in libraries [(:component-file shape-inst) :data])
|
library (dm/get-in libraries [(:component-file shape-inst) :data])
|
||||||
component (ctkl/get-component library (:component-id shape-inst))
|
component (ctkl/get-component library (:component-id shape-inst))
|
||||||
|
|
||||||
shape-main (when component
|
shape-main (when component
|
||||||
(ctf/find-remote-shape container libraries shape-inst))
|
(ctf/find-remote-shape container libraries shape-inst))
|
||||||
|
|
||||||
|
shape-inst (redirect-shaperef shape-inst shape-main)
|
||||||
|
|
||||||
initial-root? (:component-root shape-inst)
|
initial-root? (:component-root shape-inst)
|
||||||
|
|
||||||
root-inst shape-inst
|
root-inst shape-inst
|
||||||
@ -1019,11 +1038,12 @@
|
|||||||
shape-main
|
shape-main
|
||||||
root-inst
|
root-inst
|
||||||
root-main
|
root-main
|
||||||
initial-root?)
|
initial-root?
|
||||||
|
redirect-shaperef)
|
||||||
changes)))
|
changes)))
|
||||||
|
|
||||||
(defn- generate-sync-shape-inverse-recursive
|
(defn- generate-sync-shape-inverse-recursive
|
||||||
[changes container shape-inst component library file libraries shape-main root-inst root-main initial-root?]
|
[changes container shape-inst component library file libraries shape-main root-inst root-main initial-root? redirect-shaperef]
|
||||||
(shape-log :trace (:id shape-inst) container
|
(shape-log :trace (:id shape-inst) container
|
||||||
:msg "Sync shape inverse recursive"
|
:msg "Sync shape inverse recursive"
|
||||||
:shape (str (:name shape-inst))
|
:shape (str (:name shape-inst))
|
||||||
@ -1080,6 +1100,8 @@
|
|||||||
children-main (mapv #(ctn/get-shape component-container %)
|
children-main (mapv #(ctn/get-shape component-container %)
|
||||||
(:shapes shape-main))
|
(:shapes shape-main))
|
||||||
|
|
||||||
|
children-inst (map #(redirect-shaperef %) children-inst)
|
||||||
|
|
||||||
only-inst (fn [changes child-inst]
|
only-inst (fn [changes child-inst]
|
||||||
(add-shape-to-main changes
|
(add-shape-to-main changes
|
||||||
child-inst
|
child-inst
|
||||||
@ -1108,7 +1130,8 @@
|
|||||||
child-main
|
child-main
|
||||||
root-inst
|
root-inst
|
||||||
root-main
|
root-main
|
||||||
initial-root?))
|
initial-root?
|
||||||
|
redirect-shaperef))
|
||||||
|
|
||||||
swapped (fn [changes child-inst child-main]
|
swapped (fn [changes child-inst child-main]
|
||||||
(shape-log :trace (:id child-inst) container
|
(shape-log :trace (:id child-inst) container
|
||||||
@ -1750,23 +1773,6 @@
|
|||||||
(pcb/update-shapes changes [(:id dest-shape)] ctk/unhead-shape {:ignore-touched true})
|
(pcb/update-shapes changes [(:id dest-shape)] ctk/unhead-shape {:ignore-touched true})
|
||||||
changes))
|
changes))
|
||||||
|
|
||||||
(defn- check-swapped-main
|
|
||||||
[changes dest-shape origin-shape]
|
|
||||||
;; Only for direct updates (from main to copy). Check if the main shape
|
|
||||||
;; has been swapped. If so, the new component-id and component-file must
|
|
||||||
;; be put into the copy.
|
|
||||||
(if (and (= (:shape-ref dest-shape) (:id origin-shape))
|
|
||||||
(ctk/instance-head? dest-shape)
|
|
||||||
(ctk/instance-head? origin-shape)
|
|
||||||
(or (not= (:component-id dest-shape) (:component-id origin-shape))
|
|
||||||
(not= (:component-file dest-shape) (:component-file origin-shape))))
|
|
||||||
(pcb/update-shapes changes [(:id dest-shape)]
|
|
||||||
#(assoc %
|
|
||||||
:component-id (:component-id origin-shape)
|
|
||||||
:component-file (:component-file origin-shape))
|
|
||||||
{:ignore-touched true})
|
|
||||||
changes))
|
|
||||||
|
|
||||||
(defn- update-attrs
|
(defn- update-attrs
|
||||||
"The main function that implements the attribute sync algorithm. Copy
|
"The main function that implements the attribute sync algorithm. Copy
|
||||||
attributes that have changed in the origin shape to the dest shape.
|
attributes that have changed in the origin shape to the dest shape.
|
||||||
@ -1810,8 +1816,6 @@
|
|||||||
:always
|
:always
|
||||||
(check-detached-main dest-shape origin-shape)
|
(check-detached-main dest-shape origin-shape)
|
||||||
:always
|
:always
|
||||||
(check-swapped-main dest-shape origin-shape)
|
|
||||||
:always
|
|
||||||
(generate-update-tokens container dest-shape origin-shape touched omit-touched? nil))
|
(generate-update-tokens container dest-shape origin-shape touched omit-touched? nil))
|
||||||
|
|
||||||
(let [sync-group
|
(let [sync-group
|
||||||
|
|||||||
@ -148,16 +148,16 @@ Some naming conventions:
|
|||||||
:path 'one'
|
:path 'one'
|
||||||
:depth 0
|
:depth 0
|
||||||
:leaf nil
|
:leaf nil
|
||||||
:children [{:name 'two'
|
:children-fn (fn [] [{:name 'two'
|
||||||
:path 'one.two'
|
:path 'one.two'
|
||||||
:depth 1
|
:depth 1
|
||||||
:leaf nil
|
:leaf nil
|
||||||
:children [{... :name 'three'} {... :name 'four'}]}
|
:children-fn (fn [] [{... :name 'three'} {... :name 'four'}])}
|
||||||
{:name 'five'
|
{:name 'five'
|
||||||
:path 'one.five'
|
:path 'one.five'
|
||||||
:depth 1
|
:depth 1
|
||||||
:leaf {... :name 'five'}
|
:leaf {... :name 'five'}
|
||||||
:children nil}]}]"
|
...}])}]"
|
||||||
|
|
||||||
(defn- sort-by-children
|
(defn- sort-by-children
|
||||||
"Sorts segments so that those with children come first."
|
"Sorts segments so that those with children come first."
|
||||||
@ -191,7 +191,7 @@ Some naming conventions:
|
|||||||
(into (sorted-map) grouped)))
|
(into (sorted-map) grouped)))
|
||||||
|
|
||||||
(defn- build-tree-node
|
(defn- build-tree-node
|
||||||
"Builds a single tree node with computed children."
|
"Builds a single tree node with lazy children."
|
||||||
[segment-name remaining-segments separator parent-path depth]
|
[segment-name remaining-segments separator parent-path depth]
|
||||||
(let [current-path (if parent-path
|
(let [current-path (if parent-path
|
||||||
(str parent-path "." segment-name)
|
(str parent-path "." segment-name)
|
||||||
@ -208,11 +208,12 @@ Some naming conventions:
|
|||||||
:path current-path
|
:path current-path
|
||||||
:depth depth
|
:depth depth
|
||||||
:leaf leaf-segment
|
:leaf leaf-segment
|
||||||
:children (when-not is-leaf?
|
:children-fn (when-not is-leaf?
|
||||||
(let [grouped-elements (sort-and-group-segments remaining-segments separator)]
|
(fn []
|
||||||
(mapv (fn [[child-segment-name remaining-child-segments]]
|
(let [grouped-elements (sort-and-group-segments remaining-segments separator)]
|
||||||
(build-tree-node child-segment-name remaining-child-segments separator current-path (inc depth)))
|
(mapv (fn [[child-segment-name remaining-child-segments]]
|
||||||
grouped-elements)))}]
|
(build-tree-node child-segment-name remaining-child-segments separator current-path (inc depth)))
|
||||||
|
grouped-elements))))}]
|
||||||
node))
|
node))
|
||||||
|
|
||||||
(defn build-tree-root
|
(defn build-tree-root
|
||||||
|
|||||||
@ -113,19 +113,12 @@
|
|||||||
(tgen/fmap keyword)))))
|
(tgen/fmap keyword)))))
|
||||||
|
|
||||||
;; --- SPEC: email
|
;; --- SPEC: email
|
||||||
;;
|
|
||||||
;; Regex rules enforced:
|
|
||||||
;; local part - valid RFC chars, no leading/trailing dot, no consecutive dots
|
|
||||||
;; domain - labels can't start/end with hyphen, no empty labels
|
|
||||||
;; TLD - at least 2 alphabetic chars
|
|
||||||
|
|
||||||
(def email-re
|
(def email-re #"[a-zA-Z0-9_.+-\\\\]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+")
|
||||||
#"^[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+(\.[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+)*@[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*\.[a-zA-Z]{2,63}$")
|
|
||||||
|
|
||||||
(defn parse-email
|
(defn parse-email
|
||||||
[s]
|
[s]
|
||||||
(when (and (string? s) (re-matches email-re s))
|
(some->> s (re-seq email-re) first))
|
||||||
s))
|
|
||||||
|
|
||||||
(letfn [(conformer [v]
|
(letfn [(conformer [v]
|
||||||
(or (parse-email v) ::s/invalid))
|
(or (parse-email v) ::s/invalid))
|
||||||
@ -133,10 +126,11 @@
|
|||||||
(dm/str v))]
|
(dm/str v))]
|
||||||
(s/def ::email
|
(s/def ::email
|
||||||
(s/with-gen (s/conformer conformer unformer)
|
(s/with-gen (s/conformer conformer unformer)
|
||||||
#(tgen/let [local (tgen/string-alphanumeric 1 20)
|
#(as-> (tgen/let [p1 (s/gen ::not-empty-string)
|
||||||
label (tgen/string-alphanumeric 2 10)
|
p2 (s/gen ::not-empty-string)
|
||||||
tld (tgen/elements ["com" "net" "org" "io" "co" "dev"])]
|
p3 (tgen/elements ["com" "net"])]
|
||||||
(str local "@" label "." tld)))))
|
(str p1 "@" p2 "." p3)) $
|
||||||
|
(tgen/such-that (partial re-matches email-re) $ 50)))))
|
||||||
|
|
||||||
;; -- SPEC: uri
|
;; -- SPEC: uri
|
||||||
|
|
||||||
|
|||||||
@ -177,11 +177,8 @@
|
|||||||
(thc/instantiate-component component-label copy-root-label copy-root-params)))
|
(thc/instantiate-component component-label copy-root-label copy-root-params)))
|
||||||
|
|
||||||
(defn add-nested-component
|
(defn add-nested-component
|
||||||
[file
|
[file component1-label main1-root-label main1-child-label component2-label main2-root-label nested-head-label
|
||||||
component1-label main1-root-label main1-child-label
|
& {:keys [component1-params root1-params main1-child-params component2-params main2-root-params nested-head-params]}]
|
||||||
component2-label main2-root-label nested-head-label
|
|
||||||
& {:keys [component1-params root1-params main1-child-params
|
|
||||||
component2-params main2-root-params nested-head-params]}]
|
|
||||||
;; Generated shape tree:
|
;; Generated shape tree:
|
||||||
;; {:main1-root-label} [:name Frame1] # [Component :component1-label]
|
;; {:main1-root-label} [:name Frame1] # [Component :component1-label]
|
||||||
;; :main1-child-label [:name Rect1]
|
;; :main1-child-label [:name Rect1]
|
||||||
@ -207,13 +204,8 @@
|
|||||||
component2-params)))
|
component2-params)))
|
||||||
|
|
||||||
(defn add-nested-component-with-copy
|
(defn add-nested-component-with-copy
|
||||||
[file
|
[file component1-label main1-root-label main1-child-label component2-label main2-root-label nested-head-label copy2-root-label
|
||||||
component1-label main1-root-label main1-child-label
|
& {:keys [component1-params root1-params main1-child-params component2-params main2-root-params nested-head-params copy2-root-params]}]
|
||||||
component2-label main2-root-label nested-head-label
|
|
||||||
copy2-root-label
|
|
||||||
& {:keys [component1-params root1-params main1-child-params
|
|
||||||
component2-params main2-root-params nested-head-params
|
|
||||||
copy2-root-params]}]
|
|
||||||
;; Generated shape tree:
|
;; Generated shape tree:
|
||||||
;; {:main1-root-label} [:name Frame1] # [Component :component1-label]
|
;; {:main1-root-label} [:name Frame1] # [Component :component1-label]
|
||||||
;; :main1-child-label [:name Rect1]
|
;; :main1-child-label [:name Rect1]
|
||||||
@ -240,102 +232,6 @@
|
|||||||
:nested-head-params nested-head-params)
|
:nested-head-params nested-head-params)
|
||||||
(thc/instantiate-component component2-label copy2-root-label copy2-root-params)))
|
(thc/instantiate-component component2-label copy2-root-label copy2-root-params)))
|
||||||
|
|
||||||
(defn add-two-levels-nested-component
|
|
||||||
[file
|
|
||||||
component1-label main1-root-label main1-child-label
|
|
||||||
component2-label main2-root-label nested-head1-label
|
|
||||||
component3-label main3-root-label nested-head2-label nested-subhead2-label
|
|
||||||
& {:keys [component1-params root1-params main1-child-params
|
|
||||||
component2-params main2-root-params nested-head1-params
|
|
||||||
component3-params main3-root-params nested-head2-params]}]
|
|
||||||
;; Generated shape tree:
|
|
||||||
;; {:main1-root-label} [:name Frame1] # [Component :component1-label]
|
|
||||||
;; :main1-child-label [:name Rect1]
|
|
||||||
;;
|
|
||||||
;; {:main2-root-label} [:name Frame2] # [Component :component2-label]
|
|
||||||
;; :nested-head1-label [:name Frame1] @--> [Component :component1-label] :main1-root-label
|
|
||||||
;; <no-label> [:name Rect1] ---> :main1-child-label
|
|
||||||
;;
|
|
||||||
;; {:main3-root-label} [:name Frame3] # [Component :component3-label]
|
|
||||||
;; :nested-head2-label [:name Frame2] @--> [Component :component2-label] :main2-root-label
|
|
||||||
;; :nested-subhead2-label [:name Frame1] @--> [Component :component1-label] :main1-root-label
|
|
||||||
;; <no-label> [:name Rect1] ---> :main1-child-label
|
|
||||||
(-> file
|
|
||||||
(add-simple-component component1-label
|
|
||||||
main1-root-label
|
|
||||||
main1-child-label
|
|
||||||
:component-params component1-params
|
|
||||||
:root-params root1-params
|
|
||||||
:child-params main1-child-params)
|
|
||||||
(add-frame main2-root-label (merge {:name "Frame2"}
|
|
||||||
main2-root-params))
|
|
||||||
(thc/instantiate-component component1-label
|
|
||||||
nested-head1-label
|
|
||||||
(assoc nested-head1-params
|
|
||||||
:parent-label main2-root-label))
|
|
||||||
(thc/make-component component2-label
|
|
||||||
main2-root-label
|
|
||||||
component2-params)
|
|
||||||
(add-frame main3-root-label (merge {:name "Frame3"}
|
|
||||||
main3-root-params))
|
|
||||||
(thc/instantiate-component component2-label
|
|
||||||
nested-head2-label
|
|
||||||
(assoc nested-head2-params
|
|
||||||
:parent-label main3-root-label
|
|
||||||
:children-labels [nested-subhead2-label]))
|
|
||||||
(thc/make-component component3-label
|
|
||||||
main3-root-label
|
|
||||||
component3-params)))
|
|
||||||
|
|
||||||
(defn add-two-levels-nested-component-with-copy
|
|
||||||
[file
|
|
||||||
component1-label main1-root-label main1-child-label
|
|
||||||
component2-label main2-root-label nested-head1-label
|
|
||||||
component3-label main3-root-label nested-head2-label nested-subhead2-label
|
|
||||||
copy2-root-label
|
|
||||||
& {:keys [component1-params root1-params main1-child-params
|
|
||||||
component2-params main2-root-params nested-head1-params
|
|
||||||
component3-params main3-root-params nested-head2-params
|
|
||||||
copy2-root-params]}]
|
|
||||||
;; Generated shape tree:
|
|
||||||
;; {:main1-root-label} [:name Frame1] # [Component :component1-label]
|
|
||||||
;; :main1-child-label [:name Rect1]
|
|
||||||
;;
|
|
||||||
;; {:main2-root-label} [:name Frame2] # [Component :component2-label]
|
|
||||||
;; :nested-head1-label [:name Frame1] @--> [Component :component1-label] :main1-root-label
|
|
||||||
;; <no-label> [:name Rect1] ---> :main1-child-label
|
|
||||||
;;
|
|
||||||
;; {:main3-root-label} [:name Frame3] # [Component :component3-label]
|
|
||||||
;; :nested-head2-label [:name Frame2] @--> [Component :component2-label] :main2-root-label
|
|
||||||
;; :nested-subhead2-label [:name Frame1] @--> [Component :component1-label] :main1-root-label
|
|
||||||
;; <no-label> [:name Rect1] ---> :main1-child-label
|
|
||||||
;;
|
|
||||||
;; :copy2-label [:name Frame3] #--> [Component :component3-label] :main3-root-label
|
|
||||||
;; <no-label> [:name Frame2] @--> [Component :component2-label] :nested-head2-label
|
|
||||||
;; <no-label> [:name Frame1] @--> [Component :component1-label] :nested-subhead2-label
|
|
||||||
;; <no-label> [:name Rect1] ---> <no-label>
|
|
||||||
(-> file
|
|
||||||
(add-two-levels-nested-component component1-label
|
|
||||||
main1-root-label
|
|
||||||
main1-child-label
|
|
||||||
component2-label
|
|
||||||
main2-root-label
|
|
||||||
nested-head1-label
|
|
||||||
component3-label
|
|
||||||
main3-root-label
|
|
||||||
nested-head2-label
|
|
||||||
nested-subhead2-label
|
|
||||||
:component1-params component1-params
|
|
||||||
:root1-params root1-params
|
|
||||||
:main1-child-params main1-child-params
|
|
||||||
:component2-params component2-params
|
|
||||||
:main2-root-params main2-root-params
|
|
||||||
:nested-head1-params nested-head1-params
|
|
||||||
:component3-params component3-params
|
|
||||||
:main3-root-params main3-root-params
|
|
||||||
:nested-head2-params nested-head2-params)
|
|
||||||
(thc/instantiate-component component3-label copy2-root-label copy2-root-params)))
|
|
||||||
|
|
||||||
;; ----- Getters
|
;; ----- Getters
|
||||||
|
|
||||||
(defn bottom-shape-by-id
|
(defn bottom-shape-by-id
|
||||||
@ -378,18 +274,15 @@
|
|||||||
file-id
|
file-id
|
||||||
{file-id file}
|
{file-id file}
|
||||||
file-id))]
|
file-id))]
|
||||||
(thf/apply-changes file changes :validate? false)))
|
(thf/apply-changes file changes)))
|
||||||
|
|
||||||
(defn swap-component-
|
(defn swap-component
|
||||||
"Swap the specified shape by the component specified by component-tag"
|
"Swap the specified shape by the component specified by component-tag"
|
||||||
[file shape component-tag & {:keys [page-label propagate-fn keep-touched? new-shape-label library]}]
|
[file shape component-tag & {:keys [page-label propagate-fn keep-touched? new-shape-label]}]
|
||||||
(let [page (if page-label
|
(let [page (if page-label
|
||||||
(thf/get-page file page-label)
|
(thf/get-page file page-label)
|
||||||
(thf/current-page file))
|
(thf/current-page file))
|
||||||
libraries (cond-> {(:id file) file}
|
libraries {(:id file) file}
|
||||||
(some? library)
|
|
||||||
(assoc (:id library) library))
|
|
||||||
library (or library file)
|
|
||||||
|
|
||||||
orig-shapes (when keep-touched? (cfh/get-children-with-self (:objects page) (:id shape)))
|
orig-shapes (when keep-touched? (cfh/get-children-with-self (:objects page) (:id shape)))
|
||||||
|
|
||||||
@ -397,10 +290,10 @@
|
|||||||
(cll/generate-component-swap (pcb/empty-changes)
|
(cll/generate-component-swap (pcb/empty-changes)
|
||||||
(:objects page)
|
(:objects page)
|
||||||
shape
|
shape
|
||||||
(:data library)
|
(:data file)
|
||||||
page
|
page
|
||||||
libraries
|
libraries
|
||||||
(-> (thc/get-component library component-tag)
|
(-> (thc/get-component file component-tag)
|
||||||
:id)
|
:id)
|
||||||
0
|
0
|
||||||
nil
|
nil
|
||||||
@ -412,36 +305,26 @@
|
|||||||
[changes nil])
|
[changes nil])
|
||||||
|
|
||||||
|
|
||||||
file' (thf/apply-changes file changes :validate? (not propagate-fn))]
|
file' (thf/apply-changes file changes)]
|
||||||
(when new-shape-label
|
(when new-shape-label
|
||||||
(thi/rm-id! (:id new-shape))
|
(thi/rm-id! (:id new-shape))
|
||||||
(thi/set-id! new-shape-label (:id new-shape)))
|
(thi/set-id! new-shape-label (:id new-shape)))
|
||||||
(if propagate-fn
|
(if propagate-fn
|
||||||
(-> (propagate-fn file')
|
(propagate-fn file')
|
||||||
(thf/validate-file!))
|
|
||||||
file')))
|
file')))
|
||||||
|
|
||||||
(defn swap-component-in-shape
|
(defn swap-component-in-shape [file shape-tag component-tag & {:keys [page-label propagate-fn]}]
|
||||||
[file shape-tag component-tag & {:keys [page-label propagate-fn keep-touched? new-shape-label library]}]
|
(swap-component file (ths/get-shape file shape-tag :page-label page-label) component-tag :page-label page-label :propagate-fn propagate-fn))
|
||||||
(swap-component- file (ths/get-shape file shape-tag :page-label page-label)
|
|
||||||
component-tag
|
|
||||||
:page-label page-label
|
|
||||||
:propagate-fn propagate-fn
|
|
||||||
:keep-touched? keep-touched?
|
|
||||||
:new-shape-label new-shape-label
|
|
||||||
:library library))
|
|
||||||
|
|
||||||
(defn swap-component-in-first-child
|
(defn swap-component-in-first-child [file shape-tag component-tag & {:keys [page-label propagate-fn]}]
|
||||||
[file shape-tag component-tag & {:keys [page-label propagate-fn library]}]
|
|
||||||
(let [first-child-id (->> (ths/get-shape file shape-tag :page-label page-label)
|
(let [first-child-id (->> (ths/get-shape file shape-tag :page-label page-label)
|
||||||
:shapes
|
:shapes
|
||||||
first)]
|
first)]
|
||||||
(swap-component- file
|
(swap-component file
|
||||||
(ths/get-shape-by-id file first-child-id :page-label page-label)
|
(ths/get-shape-by-id file first-child-id :page-label page-label)
|
||||||
component-tag
|
component-tag
|
||||||
:page-label page-label
|
:page-label page-label
|
||||||
:propagate-fn propagate-fn
|
:propagate-fn propagate-fn)))
|
||||||
:library library)))
|
|
||||||
|
|
||||||
(defn update-color
|
(defn update-color
|
||||||
"Update the first fill color for the shape identified by shape-tag"
|
"Update the first fill color for the shape identified by shape-tag"
|
||||||
@ -456,10 +339,9 @@
|
|||||||
(assoc shape :fills (ths/sample-fills-color :fill-color color)))
|
(assoc shape :fills (ths/sample-fills-color :fill-color color)))
|
||||||
(:objects page)
|
(:objects page)
|
||||||
{})
|
{})
|
||||||
file' (thf/apply-changes file changes :validate? (not propagate-fn))]
|
file' (thf/apply-changes file changes)]
|
||||||
(if propagate-fn
|
(if propagate-fn
|
||||||
(-> (propagate-fn file')
|
(propagate-fn file')
|
||||||
(thf/validate-file!))
|
|
||||||
file')))
|
file')))
|
||||||
|
|
||||||
(defn update-bottom-color
|
(defn update-bottom-color
|
||||||
@ -475,10 +357,9 @@
|
|||||||
(assoc shape :fills (ths/sample-fills-color :fill-color color)))
|
(assoc shape :fills (ths/sample-fills-color :fill-color color)))
|
||||||
(:objects page)
|
(:objects page)
|
||||||
{})
|
{})
|
||||||
file' (thf/apply-changes file changes :validate? (not propagate-fn))]
|
file' (thf/apply-changes file changes)]
|
||||||
(if propagate-fn
|
(if propagate-fn
|
||||||
(-> (propagate-fn file')
|
(propagate-fn file')
|
||||||
(thf/validate-file!))
|
|
||||||
file')))
|
file')))
|
||||||
|
|
||||||
(defn reset-overrides [file shape & {:keys [page-label propagate-fn]}]
|
(defn reset-overrides [file shape & {:keys [page-label propagate-fn]}]
|
||||||
@ -493,10 +374,9 @@
|
|||||||
{file-id file}
|
{file-id file}
|
||||||
(ctn/make-container container :page)
|
(ctn/make-container container :page)
|
||||||
(:id shape)))
|
(:id shape)))
|
||||||
file' (thf/apply-changes file changes :validate? (not propagate-fn))]
|
file' (thf/apply-changes file changes)]
|
||||||
(if propagate-fn
|
(if propagate-fn
|
||||||
(-> (propagate-fn file')
|
(propagate-fn file')
|
||||||
(thf/validate-file!))
|
|
||||||
file')))
|
file')))
|
||||||
|
|
||||||
(defn reset-overrides-in-first-child [file shape-tag & {:keys [page-label propagate-fn]}]
|
(defn reset-overrides-in-first-child [file shape-tag & {:keys [page-label propagate-fn]}]
|
||||||
@ -518,10 +398,9 @@
|
|||||||
#{(-> (ths/get-shape file shape-tag :page-label page-label)
|
#{(-> (ths/get-shape file shape-tag :page-label page-label)
|
||||||
:id)}
|
:id)}
|
||||||
{})
|
{})
|
||||||
file' (thf/apply-changes file changes :validate? (not propagate-fn))]
|
file' (thf/apply-changes file changes)]
|
||||||
(if propagate-fn
|
(if propagate-fn
|
||||||
(-> (propagate-fn file')
|
(propagate-fn file')
|
||||||
(thf/validate-file!))
|
|
||||||
file')))
|
file')))
|
||||||
|
|
||||||
(defn duplicate-shape [file shape-tag & {:keys [page-label propagate-fn]}]
|
(defn duplicate-shape [file shape-tag & {:keys [page-label propagate-fn]}]
|
||||||
@ -540,9 +419,8 @@
|
|||||||
(:id file)) ;; file-id
|
(:id file)) ;; file-id
|
||||||
(cll/generate-duplicate-changes-update-indices (:objects page) ;; objects
|
(cll/generate-duplicate-changes-update-indices (:objects page) ;; objects
|
||||||
#{(:id shape)}))
|
#{(:id shape)}))
|
||||||
file' (thf/apply-changes file changes :validate? (not propagate-fn))]
|
file' (thf/apply-changes file changes)]
|
||||||
(if propagate-fn
|
(if propagate-fn
|
||||||
(-> (propagate-fn file')
|
(propagate-fn file')
|
||||||
(thf/validate-file!))
|
|
||||||
file')))
|
file')))
|
||||||
|
|
||||||
|
|||||||
@ -54,14 +54,12 @@
|
|||||||
([file] (validate-file! file {}))
|
([file] (validate-file! file {}))
|
||||||
([file libraries]
|
([file libraries]
|
||||||
(cfv/validate-file-schema! file)
|
(cfv/validate-file-schema! file)
|
||||||
(cfv/validate-file! file libraries)
|
(cfv/validate-file! file libraries)))
|
||||||
file))
|
|
||||||
|
|
||||||
(defn apply-changes
|
(defn apply-changes
|
||||||
[file changes & {:keys [validate?] :or {validate? true}}]
|
[file changes]
|
||||||
(let [file' (ctf/update-file-data file #(cfc/process-changes % (:redo-changes changes) true))]
|
(let [file' (ctf/update-file-data file #(cfc/process-changes % (:redo-changes changes) true))]
|
||||||
(when validate?
|
(validate-file! file')
|
||||||
(validate-file! file'))
|
|
||||||
file'))
|
file'))
|
||||||
|
|
||||||
(defn apply-undo-changes
|
(defn apply-undo-changes
|
||||||
|
|||||||
@ -82,18 +82,6 @@
|
|||||||
(:id page)
|
(:id page)
|
||||||
#(ctst/set-shape % (ctn/set-shape-attr shape attr val)))))))
|
#(ctst/set-shape % (ctn/set-shape-attr shape attr val)))))))
|
||||||
|
|
||||||
(defn update-shape-by-id
|
|
||||||
[file shape-id attr val & {:keys [page-label]}]
|
|
||||||
(let [page (if page-label
|
|
||||||
(thf/get-page file page-label)
|
|
||||||
(thf/current-page file))
|
|
||||||
shape (ctst/get-shape page shape-id)]
|
|
||||||
(update file :data
|
|
||||||
(fn [file-data]
|
|
||||||
(ctpl/update-page file-data
|
|
||||||
(:id page)
|
|
||||||
#(ctst/set-shape % (ctn/set-shape-attr shape attr val)))))))
|
|
||||||
|
|
||||||
(defn update-shape-text
|
(defn update-shape-text
|
||||||
[file shape-label attr val & {:keys [page-label]}]
|
[file shape-label attr val & {:keys [page-label]}]
|
||||||
(let [page (if page-label
|
(let [page (if page-label
|
||||||
|
|||||||
@ -37,9 +37,7 @@
|
|||||||
(defn attrs-to-styles
|
(defn attrs-to-styles
|
||||||
[attrs]
|
[attrs]
|
||||||
(reduce-kv (fn [res k v]
|
(reduce-kv (fn [res k v]
|
||||||
(if (some? v)
|
(conj res (encode-style k v)))
|
||||||
(conj res (encode-style k v))
|
|
||||||
res))
|
|
||||||
#{}
|
#{}
|
||||||
attrs))
|
attrs))
|
||||||
|
|
||||||
|
|||||||
@ -163,15 +163,11 @@
|
|||||||
Note that design tokens also are involved, although they go by an alternate
|
Note that design tokens also are involved, although they go by an alternate
|
||||||
route and thus they are not part of :sync-attrs.
|
route and thus they are not part of :sync-attrs.
|
||||||
Also when detaching a nested copy it also needs to trigger a synchronization,
|
Also when detaching a nested copy it also needs to trigger a synchronization,
|
||||||
even though :shape-ref, :component-id or :component-file are not synced
|
even though :shape-ref is not a synced attribute per se"
|
||||||
attributes per se."
|
|
||||||
[attr]
|
[attr]
|
||||||
(or (contains? sync-attrs attr)
|
(or (contains? sync-attrs attr)
|
||||||
(= :shape-ref attr)
|
(= :shape-ref attr)
|
||||||
(= :applied-tokens attr)
|
(= :applied-tokens attr)))
|
||||||
(= :component-id attr)
|
|
||||||
(= :component-file attr)
|
|
||||||
(= :component-root attr)))
|
|
||||||
|
|
||||||
(defn instance-root?
|
(defn instance-root?
|
||||||
"Check if this shape is the head of a top instance."
|
"Check if this shape is the head of a top instance."
|
||||||
|
|||||||
@ -60,9 +60,6 @@
|
|||||||
(some? objects)
|
(some? objects)
|
||||||
(assoc :objects objects)
|
(assoc :objects objects)
|
||||||
|
|
||||||
(nil? objects)
|
|
||||||
(dissoc :objects)
|
|
||||||
|
|
||||||
(some? modified-at)
|
(some? modified-at)
|
||||||
(assoc :modified-at modified-at)
|
(assoc :modified-at modified-at)
|
||||||
|
|
||||||
|
|||||||
@ -55,10 +55,6 @@
|
|||||||
[page-or-component type]
|
[page-or-component type]
|
||||||
(assoc page-or-component :type type))
|
(assoc page-or-component :type type))
|
||||||
|
|
||||||
(defn unmake-container
|
|
||||||
[container]
|
|
||||||
(dissoc container :type))
|
|
||||||
|
|
||||||
(defn page?
|
(defn page?
|
||||||
[container]
|
[container]
|
||||||
(= (:type container) :page))
|
(= (:type container) :page))
|
||||||
|
|||||||
@ -204,8 +204,7 @@
|
|||||||
|
|
||||||
(defn update-file-data
|
(defn update-file-data
|
||||||
[file f]
|
[file f]
|
||||||
(when file
|
(update file :data f))
|
||||||
(update file :data f)))
|
|
||||||
|
|
||||||
(defn containers-seq
|
(defn containers-seq
|
||||||
"Generate a sequence of all pages and all components, wrapped as containers"
|
"Generate a sequence of all pages and all components, wrapped as containers"
|
||||||
@ -226,85 +225,6 @@
|
|||||||
(ctpl/update-page file-data (:id container) f)
|
(ctpl/update-page file-data (:id container) f)
|
||||||
(ctkl/update-component file-data (:id container) f)))
|
(ctkl/update-component file-data (:id container) f)))
|
||||||
|
|
||||||
(defn update-pages
|
|
||||||
"Update all pages inside the file"
|
|
||||||
[file-data f]
|
|
||||||
(update file-data :pages-index d/update-vals
|
|
||||||
(fn [page]
|
|
||||||
(-> page
|
|
||||||
(ctn/make-container :page)
|
|
||||||
(f)
|
|
||||||
(ctn/unmake-container)))))
|
|
||||||
|
|
||||||
(defn update-components
|
|
||||||
"Update all components inside the file"
|
|
||||||
[file-data f]
|
|
||||||
(d/update-when file-data :components d/update-vals
|
|
||||||
(fn [component]
|
|
||||||
(-> component
|
|
||||||
(ctn/make-container :component)
|
|
||||||
(f)
|
|
||||||
(ctn/unmake-container)))))
|
|
||||||
|
|
||||||
(defn update-containers
|
|
||||||
"Update all pages and components inside the file"
|
|
||||||
[file-data f]
|
|
||||||
(-> file-data
|
|
||||||
(update-pages f)
|
|
||||||
(update-components f)))
|
|
||||||
|
|
||||||
(defn update-objects-tree
|
|
||||||
"Do a depth-first traversal of the shapes in a container, doing different kinds of updates.
|
|
||||||
The function f receives a shape with a context metadata with the container.
|
|
||||||
It must return a map with the following keys:
|
|
||||||
- :result -> :keep, :update or :remove
|
|
||||||
- :updated-shape -> the updated shape if result is :update"
|
|
||||||
[container f]
|
|
||||||
(letfn [(update-shape-recursive
|
|
||||||
[container shape-id]
|
|
||||||
(let [shape (ctst/get-shape container shape-id)]
|
|
||||||
(when (not shape)
|
|
||||||
(throw (ex-info "Shape not found" {:shape-id shape-id})))
|
|
||||||
(let [shape (with-meta shape {:container container})
|
|
||||||
|
|
||||||
{:keys [result updated-shape]} (f shape)
|
|
||||||
|
|
||||||
container'
|
|
||||||
(case result
|
|
||||||
:keep
|
|
||||||
container
|
|
||||||
|
|
||||||
:update
|
|
||||||
(ctst/set-shape container updated-shape)
|
|
||||||
|
|
||||||
:remove
|
|
||||||
(ctst/delete-shape container shape-id true)
|
|
||||||
|
|
||||||
(throw (ex-info "Invalid result from update function" {:result result})))]
|
|
||||||
|
|
||||||
(if (= result :remove)
|
|
||||||
container'
|
|
||||||
(reduce update-shape-recursive
|
|
||||||
container'
|
|
||||||
(:shapes shape))))))]
|
|
||||||
|
|
||||||
(let [root-id (if (ctn/page? container)
|
|
||||||
uuid/zero
|
|
||||||
(:main-instance-id container))]
|
|
||||||
|
|
||||||
(if-not (empty? (:objects container))
|
|
||||||
(update-shape-recursive container root-id)
|
|
||||||
container))))
|
|
||||||
|
|
||||||
(defn update-all-shapes
|
|
||||||
"Update all shapes in the file data, using the update-objects-tree function for each container"
|
|
||||||
[file-data f]
|
|
||||||
(when file-data
|
|
||||||
(update-containers
|
|
||||||
file-data
|
|
||||||
(fn [container]
|
|
||||||
(update-objects-tree container f)))))
|
|
||||||
|
|
||||||
;; Asset helpers
|
;; Asset helpers
|
||||||
(defn find-component-file
|
(defn find-component-file
|
||||||
[file libraries component-file]
|
[file libraries component-file]
|
||||||
@ -408,27 +328,6 @@
|
|||||||
(get-ref-shape (:data component-file) component shape :with-context? with-context?))))]
|
(get-ref-shape (:data component-file) component shape :with-context? with-context?))))]
|
||||||
(some find-ref-shape-in-head (ctn/get-parent-heads (:objects container) shape))))
|
(some find-ref-shape-in-head (ctn/get-parent-heads (:objects container) shape))))
|
||||||
|
|
||||||
(defn find-near-match
|
|
||||||
"Locate the shape that occupies the same position in the near main component.
|
|
||||||
This will be the ref-shape except if the shape is a copy subhead that has been
|
|
||||||
swapped. In this case, the near match will be the ref-shape that was before
|
|
||||||
the swap."
|
|
||||||
[file container libraries shape & {:keys [include-deleted? with-context?] :or {include-deleted? false with-context? false}}]
|
|
||||||
(let [parent-shape (ctst/get-shape container (:parent-id shape))
|
|
||||||
parent-ref-shape (when parent-shape
|
|
||||||
(find-ref-shape file container libraries parent-shape :include-deleted? include-deleted? :with-context? true))
|
|
||||||
ref-container (when parent-ref-shape
|
|
||||||
(:container (meta parent-ref-shape)))
|
|
||||||
shape-index (when parent-shape
|
|
||||||
(d/index-of (:shapes parent-shape) (:id shape)))
|
|
||||||
near-match-id (when (and parent-ref-shape shape-index)
|
|
||||||
(get (:shapes parent-ref-shape) shape-index))
|
|
||||||
near-match (when near-match-id
|
|
||||||
(cond-> (ctst/get-shape ref-container near-match-id)
|
|
||||||
with-context?
|
|
||||||
(with-meta (meta parent-ref-shape))))]
|
|
||||||
near-match))
|
|
||||||
|
|
||||||
(defn advance-shape-ref
|
(defn advance-shape-ref
|
||||||
"Get the shape-ref of the near main of the shape, recursively repeated as many times
|
"Get the shape-ref of the near main of the shape, recursively repeated as many times
|
||||||
as the given levels."
|
as the given levels."
|
||||||
|
|||||||
@ -1,47 +0,0 @@
|
|||||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
||||||
;;
|
|
||||||
;; Copyright (c) KALEIDOS INC
|
|
||||||
|
|
||||||
(ns app.common.types.organization
|
|
||||||
(:require
|
|
||||||
[app.common.schema :as sm]))
|
|
||||||
|
|
||||||
(def schema:team-with-organization
|
|
||||||
[:map
|
|
||||||
[:id ::sm/uuid]
|
|
||||||
[:is-your-penpot :boolean]
|
|
||||||
[:organization
|
|
||||||
[:map
|
|
||||||
[:id ::sm/uuid]
|
|
||||||
[:name ::sm/text]
|
|
||||||
[:slug ::sm/text]
|
|
||||||
[:owner-id ::sm/uuid]
|
|
||||||
[:avatar-bg-url ::sm/uri]
|
|
||||||
[:logo-id {:optional true} [:maybe ::sm/uuid]]]]])
|
|
||||||
|
|
||||||
(def organization->team-keys
|
|
||||||
"Mapping from organization field keys to their corresponding :organization-* team keys."
|
|
||||||
[[:id :organization-id]
|
|
||||||
[:name :organization-name]
|
|
||||||
[:custom-photo :organization-custom-photo]
|
|
||||||
[:slug :organization-slug]
|
|
||||||
[:avatar-bg-url :organization-avatar-bg-url]
|
|
||||||
[:owner-id :organization-owner-id]])
|
|
||||||
|
|
||||||
(defn apply-organization
|
|
||||||
"Updates a team map with organization fields sourced from org.
|
|
||||||
Associates each org field to the corresponding :organization-* team key when
|
|
||||||
the value is non-nil; dissociates the key otherwise. This correctly handles
|
|
||||||
both attaching an org (all values present) and detaching one (org is nil or
|
|
||||||
all fields absent)."
|
|
||||||
[team organization]
|
|
||||||
(let [id (:id organization)]
|
|
||||||
(reduce (fn [acc [org-k team-k]]
|
|
||||||
(let [v (get organization org-k)]
|
|
||||||
(if (and id (some? v))
|
|
||||||
(assoc acc team-k v)
|
|
||||||
(dissoc acc team-k))))
|
|
||||||
team
|
|
||||||
organization->team-keys)))
|
|
||||||
@ -34,8 +34,7 @@
|
|||||||
[:id ::sm/uuid]
|
[:id ::sm/uuid]
|
||||||
[:axis [::sm/one-of #{:x :y}]]
|
[:axis [::sm/one-of #{:x :y}]]
|
||||||
[:position ::sm/safe-number]
|
[:position ::sm/safe-number]
|
||||||
[:frame-id {:optional true} [:maybe ::sm/uuid]]
|
[:frame-id {:optional true} [:maybe ::sm/uuid]]])
|
||||||
[:color {:optional true} [:maybe ctc/schema:hex-color]]])
|
|
||||||
|
|
||||||
(def schema:guides
|
(def schema:guides
|
||||||
[:map-of {:gen/max 2} ::sm/uuid schema:guide])
|
[:map-of {:gen/max 2} ::sm/uuid schema:guide])
|
||||||
|
|||||||
@ -145,8 +145,7 @@
|
|||||||
[::sm/one-of stroke-caps]]
|
[::sm/one-of stroke-caps]]
|
||||||
[:stroke-color {:optional true} clr/schema:hex-color]
|
[:stroke-color {:optional true} clr/schema:hex-color]
|
||||||
[:stroke-color-gradient {:optional true} clr/schema:gradient]
|
[:stroke-color-gradient {:optional true} clr/schema:gradient]
|
||||||
[:stroke-image {:optional true} clr/schema:image]
|
[:stroke-image {:optional true} clr/schema:image]])
|
||||||
[:hidden {:optional true} :boolean]])
|
|
||||||
|
|
||||||
(def stroke-attrs
|
(def stroke-attrs
|
||||||
"A set of attrs that corresponds to stroke data type"
|
"A set of attrs that corresponds to stroke data type"
|
||||||
|
|||||||
@ -874,42 +874,6 @@
|
|||||||
(duplicate-cells :column index (inc index) ids-map)
|
(duplicate-cells :column index (inc index) ids-map)
|
||||||
(assign-cells objects))))
|
(assign-cells objects))))
|
||||||
|
|
||||||
(defn duplicate-row-at
|
|
||||||
"Duplicate source row and insert the copy at target-index (0-indexed).
|
|
||||||
Like `duplicate-row` but inserts at an arbitrary position.
|
|
||||||
Note: after add-grid-row, if target <= source the source cells shift
|
|
||||||
by +1, so we must adjust the from-index for duplicate-cells."
|
|
||||||
[shape objects source-index target-index ids-map]
|
|
||||||
(let [value (dm/get-in shape [:layout-grid-rows source-index])
|
|
||||||
;; After inserting at target-index, cells at rows >= (inc target-index)
|
|
||||||
;; get shifted +1. If target <= source, the source row shifts.
|
|
||||||
adjusted-source (if (<= target-index source-index)
|
|
||||||
(inc source-index)
|
|
||||||
source-index)]
|
|
||||||
(-> shape
|
|
||||||
(remove-cell-areas-after :row source-index)
|
|
||||||
(add-grid-row value target-index)
|
|
||||||
(duplicate-cells :row adjusted-source target-index ids-map)
|
|
||||||
(assign-cells objects))))
|
|
||||||
|
|
||||||
(defn duplicate-column-at
|
|
||||||
"Duplicate source column and insert the copy at target-index (0-indexed).
|
|
||||||
Like `duplicate-column` but inserts at an arbitrary position.
|
|
||||||
Note: after add-grid-column, if target <= source the source cells shift
|
|
||||||
by +1, so we must adjust the from-index for duplicate-cells."
|
|
||||||
[shape objects source-index target-index ids-map]
|
|
||||||
(let [value (dm/get-in shape [:layout-grid-columns source-index])
|
|
||||||
;; After inserting at target-index, cells at columns >= (inc target-index)
|
|
||||||
;; get shifted +1. If target <= source, the source column shifts.
|
|
||||||
adjusted-source (if (<= target-index source-index)
|
|
||||||
(inc source-index)
|
|
||||||
source-index)]
|
|
||||||
(-> shape
|
|
||||||
(remove-cell-areas-after :column source-index)
|
|
||||||
(add-grid-column value target-index)
|
|
||||||
(duplicate-cells :column adjusted-source target-index ids-map)
|
|
||||||
(assign-cells objects))))
|
|
||||||
|
|
||||||
(defn make-remove-cell
|
(defn make-remove-cell
|
||||||
[attr span-attr track-num]
|
[attr span-attr track-num]
|
||||||
(fn [[_ cell]]
|
(fn [[_ cell]]
|
||||||
|
|||||||
@ -16,6 +16,8 @@
|
|||||||
[app.common.types.shape.layout :as ctl]
|
[app.common.types.shape.layout :as ctl]
|
||||||
[app.common.uuid :as uuid]))
|
[app.common.uuid :as uuid]))
|
||||||
|
|
||||||
|
|
||||||
|
;; FIXME: the order of arguments seems arbitrary, container should be a first artgument
|
||||||
(defn add-shape
|
(defn add-shape
|
||||||
"Insert a shape in the tree, at the given index below the given parent or frame.
|
"Insert a shape in the tree, at the given index below the given parent or frame.
|
||||||
Update the parent as needed."
|
Update the parent as needed."
|
||||||
|
|||||||
@ -26,4 +26,3 @@
|
|||||||
[:id ::sm/uuid]
|
[:id ::sm/uuid]
|
||||||
[:name :string]])
|
[:name :string]])
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -95,9 +95,7 @@
|
|||||||
:text-direction "ltr"})
|
:text-direction "ltr"})
|
||||||
|
|
||||||
(def default-text-attrs
|
(def default-text-attrs
|
||||||
{:typography-ref-file nil
|
{:font-id "sourcesanspro"
|
||||||
:typography-ref-id nil
|
|
||||||
:font-id "sourcesanspro"
|
|
||||||
:font-family "sourcesanspro"
|
:font-family "sourcesanspro"
|
||||||
:font-variant-id "regular"
|
:font-variant-id "regular"
|
||||||
:font-size "14"
|
:font-size "14"
|
||||||
@ -356,32 +354,6 @@
|
|||||||
[k (get attrs k v)]))))
|
[k (get attrs k v)]))))
|
||||||
|
|
||||||
|
|
||||||
(defn content-has-text?
|
|
||||||
[content search]
|
|
||||||
(let [search-lower (str/lower search)]
|
|
||||||
(->> (node-seq is-text-node? content)
|
|
||||||
(some #(str/includes? (str/lower (:text %)) search-lower))
|
|
||||||
(boolean))))
|
|
||||||
|
|
||||||
(defn replace-all-case-insensitive
|
|
||||||
[text search replacement]
|
|
||||||
(let [text-lower (str/lower text)
|
|
||||||
search-lower (str/lower search)
|
|
||||||
search-len (count search)]
|
|
||||||
(loop [result "" idx 0]
|
|
||||||
(let [found (str/index-of text-lower search-lower idx)]
|
|
||||||
(if (nil? found)
|
|
||||||
(str result (subs text idx))
|
|
||||||
(recur (str result (subs text idx found) replacement)
|
|
||||||
(+ found search-len)))))))
|
|
||||||
|
|
||||||
(defn replace-text-in-content
|
|
||||||
[content search replacement]
|
|
||||||
(transform-nodes
|
|
||||||
is-text-node?
|
|
||||||
(fn [node] (update node :text replace-all-case-insensitive search replacement))
|
|
||||||
content))
|
|
||||||
|
|
||||||
(defn content->text
|
(defn content->text
|
||||||
"Given a root node of a text content extracts the texts with its associated styles"
|
"Given a root node of a text content extracts the texts with its associated styles"
|
||||||
[content]
|
[content]
|
||||||
|
|||||||
@ -136,9 +136,6 @@
|
|||||||
(def token-name-validation-regex
|
(def token-name-validation-regex
|
||||||
#"^[a-zA-Z0-9_-][a-zA-Z0-9$_-]*(\.[a-zA-Z0-9$_-]+)*$")
|
#"^[a-zA-Z0-9_-][a-zA-Z0-9$_-]*(\.[a-zA-Z0-9$_-]+)*$")
|
||||||
|
|
||||||
(def token-node-name-validation-regex
|
|
||||||
#"^[a-zA-Z0-9_-][a-zA-Z0-9$_-]*(\.[a-zA-Z0-9$_-]+)*$")
|
|
||||||
|
|
||||||
(def schema:token-name
|
(def schema:token-name
|
||||||
"A token name can contains letters, numbers, underscores the character $ and dots, but
|
"A token name can contains letters, numbers, underscores the character $ and dots, but
|
||||||
not start with $ or end with a dot. The $ character does not have any special meaning,
|
not start with $ or end with a dot. The $ character does not have any special meaning,
|
||||||
@ -156,14 +153,6 @@
|
|||||||
:gen/gen sg/text}
|
:gen/gen sg/text}
|
||||||
token-ref-validation-regex])
|
token-ref-validation-regex])
|
||||||
|
|
||||||
(def schema:token-node-name
|
|
||||||
"A token node name can contains letters, numbers, underscores and the character $, but
|
|
||||||
not start with $ or a dot, or end with a dot. The $ character does not have any special meaning,
|
|
||||||
but dots separate token groups (e.g. color.primary.background)."
|
|
||||||
[:re {:title "TokenNodeName"
|
|
||||||
:gen/gen sg/text}
|
|
||||||
token-node-name-validation-regex])
|
|
||||||
|
|
||||||
(def schema:token-type
|
(def schema:token-type
|
||||||
[::sm/one-of {:decode/json (fn [type]
|
[::sm/one-of {:decode/json (fn [type]
|
||||||
(if (string? type)
|
(if (string? type)
|
||||||
@ -532,32 +521,31 @@
|
|||||||
|
|
||||||
(def tokens-by-input
|
(def tokens-by-input
|
||||||
"A map from input name to applicable token for that input."
|
"A map from input name to applicable token for that input."
|
||||||
{:width [:sizing :dimensions]
|
{:width #{:sizing :dimensions}
|
||||||
:height [:sizing :dimensions]
|
:height #{:sizing :dimensions}
|
||||||
:max-width [:sizing :dimensions]
|
:max-width #{:sizing :dimensions}
|
||||||
:max-height [:sizing :dimensions]
|
:max-height #{:sizing :dimensions}
|
||||||
:min-width [:sizing :dimensions]
|
:min-width #{:sizing :dimensions}
|
||||||
:min-height [:sizing :dimensions]
|
:min-height #{:sizing :dimensions}
|
||||||
:x [:dimensions]
|
:x #{:dimensions}
|
||||||
:y [:dimensions]
|
:y #{:dimensions}
|
||||||
:rotation [:rotation :number]
|
:rotation #{:number :rotation}
|
||||||
:border-radius [:border-radius :dimensions]
|
:border-radius #{:border-radius :dimensions}
|
||||||
:row-gap [:spacing :dimensions]
|
:row-gap #{:spacing :dimensions}
|
||||||
:column-gap [:spacing :dimensions]
|
:column-gap #{:spacing :dimensions}
|
||||||
:horizontal-padding [:spacing :dimensions]
|
:horizontal-padding #{:spacing :dimensions}
|
||||||
:vertical-padding [:spacing :dimensions]
|
:vertical-padding #{:spacing :dimensions}
|
||||||
:sided-paddings [:spacing :dimensions]
|
:sided-paddings #{:spacing :dimensions}
|
||||||
:horizontal-margin [:spacing :dimensions]
|
:horizontal-margin #{:spacing :dimensions}
|
||||||
:vertical-margin [:spacing :dimensions]
|
:vertical-margin #{:spacing :dimensions}
|
||||||
:sided-margins [:spacing :dimensions]
|
:sided-margins #{:spacing :dimensions}
|
||||||
:line-height [:line-height :number]
|
:line-height #{:line-height :number}
|
||||||
:opacity [:opacity]
|
:opacity #{:opacity}
|
||||||
:stroke-width [:stroke-width :dimensions]
|
:stroke-width #{:stroke-width :dimensions}
|
||||||
:font-size [:font-size]
|
:font-size #{:font-size}
|
||||||
:letter-spacing [:letter-spacing]
|
:letter-spacing #{:letter-spacing}
|
||||||
:fill [:color]
|
:fill #{:color}
|
||||||
:stroke-color [:color]
|
:stroke-color #{:color}})
|
||||||
:typography [:typography]})
|
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; HELPERS for tokens application
|
;; HELPERS for tokens application
|
||||||
|
|||||||
@ -153,18 +153,6 @@
|
|||||||
tokens)]
|
tokens)]
|
||||||
(group-by :type tokens')))
|
(group-by :type tokens')))
|
||||||
|
|
||||||
(defn rename-path
|
|
||||||
"Renames a node or token path segment with a new name.
|
|
||||||
If token is provided, it renames a token path, otherwise it renames a node path."
|
|
||||||
([node new-name]
|
|
||||||
(rename-path node nil new-name))
|
|
||||||
([node token new-name]
|
|
||||||
(let [element (if token (:name token) (:path node))
|
|
||||||
split-path (cpn/split-path element :separator ".")
|
|
||||||
updated-split-element-name (assoc split-path (:depth node) new-name)
|
|
||||||
new-element-path (cpn/join-path updated-split-element-name :separator "." :with-spaces? false)]
|
|
||||||
new-element-path)))
|
|
||||||
|
|
||||||
;; === Token Set
|
;; === Token Set
|
||||||
|
|
||||||
(defprotocol ITokenSet
|
(defprotocol ITokenSet
|
||||||
@ -932,7 +920,6 @@ Will return a value that matches this schema:
|
|||||||
`:all` All of the nested sets are active
|
`:all` All of the nested sets are active
|
||||||
`:partial` Mixed active state of nested sets")
|
`:partial` Mixed active state of nested sets")
|
||||||
(get-tokens-in-active-sets [_] "set of set names that are active in the the active themes")
|
(get-tokens-in-active-sets [_] "set of set names that are active in the the active themes")
|
||||||
(get-tokens-in-active-sets-force [_ force-set-id] "same as above but forcing a set to be active, even if it's not in the active themes")
|
|
||||||
(get-all-tokens [_] "all tokens in the lib, as a sequence")
|
(get-all-tokens [_] "all tokens in the lib, as a sequence")
|
||||||
(get-all-tokens-map [_] "all tokens in the lib, as a map name -> token")
|
(get-all-tokens-map [_] "all tokens in the lib, as a map name -> token")
|
||||||
(get-tokens [_ set-id] "return a map of tokens in the set, indexed by token-name"))
|
(get-tokens [_ set-id] "return a map of tokens in the set, indexed by token-name"))
|
||||||
@ -1330,21 +1317,6 @@ Will return a value that matches this schema:
|
|||||||
active-set-names)]
|
active-set-names)]
|
||||||
tokens))
|
tokens))
|
||||||
|
|
||||||
(get-tokens-in-active-sets-force [this force-set-id]
|
|
||||||
(let [theme-set-names (get-active-themes-set-names this)
|
|
||||||
all-set-names (get-set-names this)
|
|
||||||
force-set (get-set this force-set-id)
|
|
||||||
active-set-names (cond-> (filter theme-set-names all-set-names)
|
|
||||||
(some? force-set)
|
|
||||||
(conj (get-name force-set)))
|
|
||||||
|
|
||||||
tokens (reduce (fn [tokens set-name]
|
|
||||||
(let [set (get-set-by-name this set-name)]
|
|
||||||
(merge tokens (get-tokens- set))))
|
|
||||||
(d/ordered-map)
|
|
||||||
active-set-names)]
|
|
||||||
tokens))
|
|
||||||
|
|
||||||
(get-all-tokens [this]
|
(get-all-tokens [this]
|
||||||
(mapcat #(vals (get-tokens- %))
|
(mapcat #(vals (get-tokens- %))
|
||||||
(get-sets this)))
|
(get-sets this)))
|
||||||
@ -1521,30 +1493,6 @@ Will return a value that matches this schema:
|
|||||||
(seq)
|
(seq)
|
||||||
(boolean)))))
|
(boolean)))))
|
||||||
|
|
||||||
(defn update-tokens-group
|
|
||||||
"Updates the active tokens path when renaming a group node.
|
|
||||||
- Filters tokens whose path matches the current path prefix
|
|
||||||
- Replaces the token name with the new name
|
|
||||||
- Updates the :path value in the token object
|
|
||||||
|
|
||||||
active-tokens: map of token-name to token-object for all active tokens in the set
|
|
||||||
current-path: the path of the group being renamed, e.g. \"foo.bar\"
|
|
||||||
current-name: the current name of the group being renamed, e.g. \"bar\"
|
|
||||||
new-name: the new name for the group being renamed, e.g. \"baz\""
|
|
||||||
|
|
||||||
[active-tokens current-path current-name new-name]
|
|
||||||
(let [path-prefix (str/replace current-path current-name "")]
|
|
||||||
(mapv (fn [[token-path token-obj]]
|
|
||||||
(if (str/starts-with? token-path path-prefix)
|
|
||||||
(let [new-token-path (str/replace token-path current-name new-name)
|
|
||||||
new-token-obj (-> token-obj
|
|
||||||
(assoc :name new-token-path)
|
|
||||||
(cond-> (:path token-obj)
|
|
||||||
(assoc :path (str/replace (:path token-obj) current-name new-name))))]
|
|
||||||
[new-token-path new-token-obj])
|
|
||||||
[token-path token-obj]))
|
|
||||||
active-tokens)))
|
|
||||||
|
|
||||||
;; === Import / Export from JSON format
|
;; === Import / Export from JSON format
|
||||||
|
|
||||||
;; Supported formats:
|
;; Supported formats:
|
||||||
|
|||||||
@ -60,9 +60,8 @@
|
|||||||
:cljs (uuid (impl/v4))))
|
:cljs (uuid (impl/v4))))
|
||||||
|
|
||||||
(defn custom
|
(defn custom
|
||||||
"Generate a uuid using directly the given number (specified as one or two long integers)"
|
([a] #?(:clj (UUID. 0 a) :cljs (uuid (impl/custom 0 a))))
|
||||||
([low] #?(:clj (UUID. 0 low) :cljs (uuid (impl/custom 0 low))))
|
([b a] #?(:clj (UUID. b a) :cljs (uuid (impl/custom b a)))))
|
||||||
([high low] #?(:clj (UUID. high low) :cljs (uuid (impl/custom high low)))))
|
|
||||||
|
|
||||||
(def zero (uuid "00000000-0000-0000-0000-000000000000"))
|
(def zero (uuid "00000000-0000-0000-0000-000000000000"))
|
||||||
|
|
||||||
@ -138,22 +137,6 @@
|
|||||||
(+ (clojure.lang.Murmur3/hashLong a)
|
(+ (clojure.lang.Murmur3/hashLong a)
|
||||||
(clojure.lang.Murmur3/hashLong b)))))
|
(clojure.lang.Murmur3/hashLong b)))))
|
||||||
|
|
||||||
;; Fake uuids generator
|
|
||||||
(def ^:private fake-ids (atom 0))
|
|
||||||
|
|
||||||
(defn reset-fake!
|
|
||||||
"Reset the fake uuid counter to 0, for reproducible results across tests."
|
|
||||||
[]
|
|
||||||
(reset! fake-ids 0))
|
|
||||||
|
|
||||||
(defn next-fake
|
|
||||||
"When you need predictable uuids, for example when debugging a failing test, wrap the code with
|
|
||||||
(with-redefs [uuid/next uuid/next-fake]
|
|
||||||
...tested code...)"
|
|
||||||
[]
|
|
||||||
(-> (swap! fake-ids inc)
|
|
||||||
(custom)))
|
|
||||||
|
|
||||||
;; Commented code used for debug
|
;; Commented code used for debug
|
||||||
;; #?(:cljs
|
;; #?(:cljs
|
||||||
;; (defn ^:export test-uuid
|
;; (defn ^:export test-uuid
|
||||||
|
|||||||
@ -1,151 +0,0 @@
|
|||||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
||||||
;;
|
|
||||||
;; Copyright (c) KALEIDOS INC
|
|
||||||
|
|
||||||
(ns common-tests.attrs-test
|
|
||||||
(:require
|
|
||||||
[app.common.attrs :as attrs]
|
|
||||||
[clojure.test :as t]))
|
|
||||||
|
|
||||||
(t/deftest get-attrs-multi-same-value
|
|
||||||
(t/testing "returns value when all objects have the same attribute value"
|
|
||||||
(let [objs [{:attr "red"}
|
|
||||||
{:attr "red"}
|
|
||||||
{:attr "red"}]
|
|
||||||
result (attrs/get-attrs-multi objs [:attr])]
|
|
||||||
(t/is (= {:attr "red"} result))))
|
|
||||||
|
|
||||||
(t/testing "returns nil when all objects have nil value"
|
|
||||||
(let [objs [{:attr nil}
|
|
||||||
{:attr nil}]
|
|
||||||
result (attrs/get-attrs-multi objs [:attr])]
|
|
||||||
(t/is (= {:attr nil} result)))))
|
|
||||||
|
|
||||||
(t/deftest get-attrs-multi-different-values
|
|
||||||
(t/testing "returns :multiple when objects have different concrete values"
|
|
||||||
(let [objs [{:attr "red"}
|
|
||||||
{:attr "blue"}]
|
|
||||||
result (attrs/get-attrs-multi objs [:attr])]
|
|
||||||
(t/is (= {:attr :multiple} result)))))
|
|
||||||
|
|
||||||
(t/deftest get-attrs-multi-missing-key
|
|
||||||
(t/testing "returns value when one object has the attribute and another doesn't"
|
|
||||||
(let [objs [{:attr "red"}
|
|
||||||
{:other "value"}]
|
|
||||||
result (attrs/get-attrs-multi objs [:attr])]
|
|
||||||
(t/is (= {:attr "red"} result))))
|
|
||||||
|
|
||||||
(t/testing "returns value when one object has UUID and another is missing"
|
|
||||||
(let [uuid #uuid "550e8400-e29b-41d4-a716-446655440000"
|
|
||||||
objs [{:attr uuid}
|
|
||||||
{:other "value"}]
|
|
||||||
result (attrs/get-attrs-multi objs [:attr])]
|
|
||||||
(t/is (= {:attr uuid} result))))
|
|
||||||
|
|
||||||
(t/testing "returns :multiple when some objects have the key and some don't"
|
|
||||||
(let [objs [{:attr "red"}
|
|
||||||
{:other "value"}
|
|
||||||
{:attr "blue"}]
|
|
||||||
result (attrs/get-attrs-multi objs [:attr])]
|
|
||||||
(t/is (= {:attr :multiple} result))))
|
|
||||||
|
|
||||||
(t/testing "returns nil when one object has nil and another is missing"
|
|
||||||
(let [objs [{:attr nil}
|
|
||||||
{:other "value"}]
|
|
||||||
result (attrs/get-attrs-multi objs [:attr])]
|
|
||||||
(t/is (= {:attr nil} result)))))
|
|
||||||
|
|
||||||
(t/deftest get-attrs-multi-all-missing
|
|
||||||
(t/testing "all missing → attribute NOT included in result"
|
|
||||||
(let [objs [{:other "value"}
|
|
||||||
{:different "data"}]
|
|
||||||
result (attrs/get-attrs-multi objs [:attr])]
|
|
||||||
(t/is (= {} result)
|
|
||||||
"Attribute should not be in result when all objects are missing")))
|
|
||||||
|
|
||||||
(t/testing "all missing with empty maps → attribute NOT included"
|
|
||||||
(let [objs [{} {}]
|
|
||||||
result (attrs/get-attrs-multi objs [:attr])]
|
|
||||||
(t/is (= {} result)
|
|
||||||
"Attribute should not be in result"))))
|
|
||||||
|
|
||||||
(t/deftest get-attrs-multi-multiple-attributes
|
|
||||||
(t/testing "handles multiple attributes with different merge results"
|
|
||||||
(let [objs [{:attr1 "red" :attr2 "blue"}
|
|
||||||
{:attr1 "red" :attr2 "green"}
|
|
||||||
{:attr1 "red"}] ; :attr2 missing
|
|
||||||
result (attrs/get-attrs-multi objs [:attr1 :attr2])]
|
|
||||||
(t/is (= {:attr1 "red" :attr2 :multiple} result))))
|
|
||||||
|
|
||||||
(t/testing "handles mixed scenarios: same, different, and missing"
|
|
||||||
(let [uuid #uuid "550e8400-e29b-41d4-a716-446655440000"
|
|
||||||
uuid2 #uuid "550e8400-e29b-41d4-a716-446655440001"
|
|
||||||
objs [{:id :a :ref uuid}
|
|
||||||
{:id :b :ref uuid2}
|
|
||||||
{:id :c}] ; :ref missing
|
|
||||||
result (attrs/get-attrs-multi objs [:id :ref])]
|
|
||||||
(t/is (= {:id :multiple :ref :multiple} result)))))
|
|
||||||
|
|
||||||
(t/deftest get-attrs-multi-typography-ref-id-scenario
|
|
||||||
(t/testing "the specific bug scenario: typography-ref-id with UUID vs missing"
|
|
||||||
(let [uuid #uuid "550e8400-e29b-41d4-a716-446655440000"
|
|
||||||
;; Shape 1 has typography-ref-id with a UUID
|
|
||||||
shape1 {:id :shape1 :typography-ref-id uuid}
|
|
||||||
;; Shape 2 does NOT have typography-ref-id at all
|
|
||||||
shape2 {:id :shape2}
|
|
||||||
result (attrs/get-attrs-multi [shape1 shape2] [:typography-ref-id])]
|
|
||||||
(t/is (= {:typography-ref-id uuid} result))))
|
|
||||||
|
|
||||||
(t/testing "both shapes missing → attribute NOT included in result"
|
|
||||||
(let [shape1 {:id :shape1}
|
|
||||||
shape2 {:id :shape2}
|
|
||||||
result (attrs/get-attrs-multi [shape1 shape2] [:typography-ref-id])]
|
|
||||||
(t/is (= {} result)
|
|
||||||
"Expected empty map when all shapes are missing the attribute"))))
|
|
||||||
|
|
||||||
(t/deftest get-attrs-multi-bug-missing-vs-present
|
|
||||||
(t/testing "BUG FIXED: one shape has :typography-ref-id, other does NOT → returns uuid"
|
|
||||||
(let [uuid #uuid "550e8400-e29b-41d4-a716-446655440000"
|
|
||||||
shape1 {:id :shape1 :typography-ref-id uuid}
|
|
||||||
shape2 {:id :shape2}
|
|
||||||
result (attrs/get-attrs-multi [shape1 shape2] [:typography-ref-id])]
|
|
||||||
(t/is (= {:typography-ref-id uuid} result))))
|
|
||||||
|
|
||||||
(t/testing "both missing → empty map (attribute not in result)"
|
|
||||||
(let [shape1 {:id :shape1}
|
|
||||||
shape2 {:id :shape2}
|
|
||||||
result (attrs/get-attrs-multi [shape1 shape2] [:typography-ref-id])]
|
|
||||||
(t/is (= {} result)
|
|
||||||
"Expected empty map when all shapes are missing the attribute")))
|
|
||||||
|
|
||||||
(t/testing "both equal values → return the value"
|
|
||||||
(let [uuid #uuid "550e8400-e29b-41d4-a716-446655440000"
|
|
||||||
shape1 {:id :shape1 :typography-ref-id uuid}
|
|
||||||
shape2 {:id :shape2 :typography-ref-id uuid}
|
|
||||||
result (attrs/get-attrs-multi [shape1 shape2] [:typography-ref-id])]
|
|
||||||
(t/is (= {:typography-ref-id uuid} result))))
|
|
||||||
|
|
||||||
(t/testing "different values → return :multiple"
|
|
||||||
(let [uuid1 #uuid "550e8400-e29b-41d4-a716-446655440000"
|
|
||||||
uuid2 #uuid "550e8400-e29b-41d4-a716-446655440001"
|
|
||||||
shape1 {:id :shape1 :typography-ref-id uuid1}
|
|
||||||
shape2 {:id :shape2 :typography-ref-id uuid2}
|
|
||||||
result (attrs/get-attrs-multi [shape1 shape2] [:typography-ref-id])]
|
|
||||||
(t/is (= {:typography-ref-id :multiple} result)))))
|
|
||||||
|
|
||||||
(t/deftest get-attrs-multi-default-equal
|
|
||||||
(t/testing "numbers use close? for equality"
|
|
||||||
(let [objs [{:value 1.0}
|
|
||||||
{:value 1.0000001}]
|
|
||||||
result (attrs/get-attrs-multi objs [:value])]
|
|
||||||
(t/is (= {:value 1.0} result)
|
|
||||||
"Numbers within tolerance should be considered equal")))
|
|
||||||
|
|
||||||
(t/testing "different floating point positions beyond tolerance are :multiple"
|
|
||||||
(let [objs [{:x -26}
|
|
||||||
{:x -153}]
|
|
||||||
result (attrs/get-attrs-multi objs [:x])]
|
|
||||||
(t/is (= {:x :multiple} result)
|
|
||||||
"Different positions should be :multiple"))))
|
|
||||||
@ -28,14 +28,6 @@
|
|||||||
(t/is (not (d/in-range? 5 -1)))
|
(t/is (not (d/in-range? 5 -1)))
|
||||||
(t/is (not (d/in-range? 0 0))))
|
(t/is (not (d/in-range? 0 0))))
|
||||||
|
|
||||||
(t/deftest get-initials-test
|
|
||||||
(t/is (= "JD" (d/get-initials "John Doe")))
|
|
||||||
(t/is (= "A" (d/get-initials "acme")))
|
|
||||||
(t/is (= "AB" (d/get-initials "123 Alpha ## beta")))
|
|
||||||
(t/is (= "PD" (d/get-initials " penpot design tool ")))
|
|
||||||
(t/is (= "" (d/get-initials nil)))
|
|
||||||
(t/is (= "" (d/get-initials "!!! ???"))))
|
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; Ordered Data Structures
|
;; Ordered Data Structures
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
@ -284,48 +276,6 @@
|
|||||||
(t/is (= (d/nth-index-of "abc*def*ghi" "*" 2) 7))
|
(t/is (= (d/nth-index-of "abc*def*ghi" "*" 2) 7))
|
||||||
(t/is (= (d/nth-index-of "abc*def*ghi" "*" 3) nil)))
|
(t/is (= (d/nth-index-of "abc*def*ghi" "*" 3) nil)))
|
||||||
|
|
||||||
(t/deftest natural-sort-by-test
|
|
||||||
(t/is (= (d/natural-sort-by identity ["10" "2" "1" "11" "3" "30"])
|
|
||||||
["1" "2" "3" "10" "11" "30"]))
|
|
||||||
(t/is (= (d/natural-sort-by identity ["banana" "apple" "cherry"])
|
|
||||||
["apple" "banana" "cherry"]))
|
|
||||||
(t/is (= (d/natural-sort-by identity ["size10" "size2" "size1" "size20" "size3"])
|
|
||||||
["size1" "size2" "size3" "size10" "size20"]))
|
|
||||||
(t/is (= (d/natural-sort-by identity ["b1" "a2" "a10" "a1"])
|
|
||||||
["a1" "a2" "a10" "b1"]))
|
|
||||||
(t/is (= (d/natural-sort-by identity []) []))
|
|
||||||
(t/is (= (d/natural-sort-by identity ["solo"]) ["solo"]))
|
|
||||||
(t/is (= (d/natural-sort-by identity ["b" "a" "a" "c"])
|
|
||||||
["a" "a" "b" "c"]))
|
|
||||||
(t/is (= (d/natural-sort-by :name
|
|
||||||
[{:name "big"} {:name "small"} {:name "medium"}])
|
|
||||||
[{:name "big"} {:name "medium"} {:name "small"}]))
|
|
||||||
(t/is (= (d/natural-sort-by :name
|
|
||||||
[{:name "size10"} {:name "size2"} {:name "size1"}])
|
|
||||||
[{:name "size1"} {:name "size2"} {:name "size10"}]))
|
|
||||||
(t/is (= (d/natural-sort-by :name
|
|
||||||
[{:name "border-radius-10"}
|
|
||||||
{:name "border-radius-2"}
|
|
||||||
{:name "border-radius-1"}])
|
|
||||||
[{:name "border-radius-1"}
|
|
||||||
{:name "border-radius-2"}
|
|
||||||
{:name "border-radius-10"}]))
|
|
||||||
(t/is (= (d/natural-sort-by :name
|
|
||||||
[{:name "border-10-radius"}
|
|
||||||
{:name "border-2-radius"}
|
|
||||||
{:name "border-1-radius"}])
|
|
||||||
[{:name "border-1-radius"}
|
|
||||||
{:name "border-2-radius"}
|
|
||||||
{:name "border-10-radius"}]))
|
|
||||||
(t/is (= (d/natural-sort-by :name
|
|
||||||
[{:name "border-10-radius"}
|
|
||||||
{:name "border-2-extra"}
|
|
||||||
{:name "border-2-radius"}
|
|
||||||
{:name "border-1-radius"}])
|
|
||||||
[{:name "border-1-radius"}
|
|
||||||
{:name "border-2-extra"}
|
|
||||||
{:name "border-2-radius"}
|
|
||||||
{:name "border-10-radius"}])))
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; Lazy / sequence helpers
|
;; Lazy / sequence helpers
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|||||||
@ -1,787 +0,0 @@
|
|||||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
||||||
;;
|
|
||||||
;; Copyright (c) KALEIDOS INC
|
|
||||||
|
|
||||||
(ns common-tests.files.comp-processors-test
|
|
||||||
(:require
|
|
||||||
[app.common.data :as d]
|
|
||||||
[app.common.files.comp-processors :as cfcp]
|
|
||||||
[app.common.test-helpers.components :as thc]
|
|
||||||
[app.common.test-helpers.compositions :as tho]
|
|
||||||
[app.common.test-helpers.files :as thf]
|
|
||||||
[app.common.test-helpers.ids-map :as thi]
|
|
||||||
[app.common.test-helpers.shapes :as ths]
|
|
||||||
[app.common.types.component :as ctk]
|
|
||||||
[app.common.types.components-list :as ctkl]
|
|
||||||
[app.common.types.file :as ctf]
|
|
||||||
[clojure.test :as t]))
|
|
||||||
|
|
||||||
(t/deftest test-remove-unneeded-objects-in-components
|
|
||||||
|
|
||||||
(t/testing "nil file should return nil"
|
|
||||||
(let [file nil
|
|
||||||
file' (ctf/update-file-data file cfcp/remove-unneeded-objects-in-components)]
|
|
||||||
(t/is (nil? file'))))
|
|
||||||
|
|
||||||
(t/testing "empty file should not need any action"
|
|
||||||
(let [file (thf/sample-file :file1)
|
|
||||||
file' (ctf/update-file-data file cfcp/remove-unneeded-objects-in-components)]
|
|
||||||
(t/is (empty? (d/map-diff file file')))))
|
|
||||||
|
|
||||||
(t/testing "file without components should not need any action"
|
|
||||||
(let [file
|
|
||||||
(-> (thf/sample-file :file1)
|
|
||||||
(tho/add-frame-with-child :frame1 :shape1))
|
|
||||||
|
|
||||||
file' (ctf/update-file-data file cfcp/remove-unneeded-objects-in-components)]
|
|
||||||
|
|
||||||
(t/is (empty? (d/map-diff file file')))))
|
|
||||||
|
|
||||||
(t/testing "file with non deleted components should not need any action"
|
|
||||||
(let [file
|
|
||||||
(-> (thf/sample-file :file1)
|
|
||||||
(tho/add-simple-component :component1 :frame1 :shape1))
|
|
||||||
|
|
||||||
file' (ctf/update-file-data file cfcp/remove-unneeded-objects-in-components)]
|
|
||||||
|
|
||||||
(t/is (empty? (d/map-diff file file')))))
|
|
||||||
|
|
||||||
(t/testing "file with deleted components should not need any action"
|
|
||||||
(let [file
|
|
||||||
(-> (thf/sample-file :file1)
|
|
||||||
(tho/add-simple-component :component1 :frame1 :shape1)
|
|
||||||
(tho/delete-shape :frame1))
|
|
||||||
|
|
||||||
file' (ctf/update-file-data file cfcp/remove-unneeded-objects-in-components)]
|
|
||||||
|
|
||||||
(t/is (empty? (d/map-diff file file')))))
|
|
||||||
|
|
||||||
(t/testing "file with non deleted components with :objects nil should remove it"
|
|
||||||
(let [file
|
|
||||||
(-> (thf/sample-file :file1)
|
|
||||||
(tho/add-simple-component :component1 :frame1 :shape1)
|
|
||||||
(thc/update-component :component1 {:objects nil}))
|
|
||||||
|
|
||||||
file' (ctf/update-file-data file cfcp/remove-unneeded-objects-in-components)
|
|
||||||
|
|
||||||
diff (d/map-diff file file')
|
|
||||||
|
|
||||||
expected-diff {:data
|
|
||||||
{:components
|
|
||||||
{(thi/id :component1)
|
|
||||||
{}}}}]
|
|
||||||
|
|
||||||
(t/is (= expected-diff diff))))
|
|
||||||
|
|
||||||
(t/testing "file with non deleted components with :objects should remove it"
|
|
||||||
(let [file
|
|
||||||
(-> (thf/sample-file :file1)
|
|
||||||
(tho/add-simple-component :component1 :frame1 :shape1)
|
|
||||||
(thc/update-component :component1 {:objects {:sample 777}}))
|
|
||||||
|
|
||||||
file' (ctf/update-file-data file cfcp/remove-unneeded-objects-in-components)
|
|
||||||
|
|
||||||
diff (d/map-diff file file')
|
|
||||||
|
|
||||||
expected-diff {:data
|
|
||||||
{:components
|
|
||||||
{(thi/id :component1)
|
|
||||||
{:objects
|
|
||||||
[{:sample 777} nil]}}}}]
|
|
||||||
|
|
||||||
(t/is (= expected-diff diff))))
|
|
||||||
|
|
||||||
(t/testing "file with deleted components without :objects should add an empty one"
|
|
||||||
(let [file
|
|
||||||
(-> (thf/sample-file :file1)
|
|
||||||
(tho/add-simple-component :component1 :frame1 :shape1)
|
|
||||||
(tho/delete-shape :frame1)
|
|
||||||
(ctf/update-file-data
|
|
||||||
(fn [file-data]
|
|
||||||
(ctkl/update-component file-data (thi/id :component1) #(dissoc % :objects)))))
|
|
||||||
|
|
||||||
file' (ctf/update-file-data file cfcp/remove-unneeded-objects-in-components)
|
|
||||||
|
|
||||||
diff (d/map-diff file file')
|
|
||||||
|
|
||||||
expected-diff {:data
|
|
||||||
{:components
|
|
||||||
{(thi/id :component1)
|
|
||||||
{:objects
|
|
||||||
[nil {}]}}}}]
|
|
||||||
|
|
||||||
(t/is (= expected-diff diff)))))
|
|
||||||
|
|
||||||
(t/deftest test-fix-missing-swap-slots
|
|
||||||
|
|
||||||
(t/testing "nil file should return nil"
|
|
||||||
(let [file nil
|
|
||||||
file' (ctf/update-file-data file #(cfcp/fix-missing-swap-slots % {}))]
|
|
||||||
(t/is (nil? file'))))
|
|
||||||
|
|
||||||
(t/testing "empty file should not need any action"
|
|
||||||
(let [file (thf/sample-file :file1)
|
|
||||||
file' (ctf/update-file-data file #(cfcp/fix-missing-swap-slots % {}))]
|
|
||||||
(t/is (empty? (d/map-diff file file')))))
|
|
||||||
|
|
||||||
(t/testing "file without components should not need any action"
|
|
||||||
(let [file
|
|
||||||
;; :frame1 [:name Frame1]
|
|
||||||
;; :child1 [:name Rect1]
|
|
||||||
(-> (thf/sample-file :file1)
|
|
||||||
(tho/add-frame-with-child :frame1 :shape1))
|
|
||||||
|
|
||||||
file' (ctf/update-file-data file #(cfcp/fix-missing-swap-slots % {}))]
|
|
||||||
|
|
||||||
(t/is (empty? (d/map-diff file file')))))
|
|
||||||
|
|
||||||
(t/testing "file with nested not swapped components should not need any action"
|
|
||||||
(let [file
|
|
||||||
;; {:main1-root} [:name Frame1] # [Component :component1]
|
|
||||||
;; :main1-child [:name Rect1]
|
|
||||||
;;
|
|
||||||
;; {:main2-root} [:name Frame2] # [Component :component2]
|
|
||||||
;; :nested-head [:name Frame1] @--> [Component :component1] :main1-root
|
|
||||||
;; <no-label> [:name Rect1] ---> :main1-child
|
|
||||||
;;
|
|
||||||
;; :copy2-root [:name Frame2] #--> [Component :component2] :main2-root
|
|
||||||
;; <no-label> [:name Frame1] @--> [Component :component1] :nested-head
|
|
||||||
;; <no-label> [:name Rect1] ---> <no-label>
|
|
||||||
(-> (thf/sample-file :file1)
|
|
||||||
(tho/add-nested-component-with-copy :component1 :main1-root :main1-child
|
|
||||||
:component2 :main2-root :nested-head
|
|
||||||
:copy2-root))
|
|
||||||
|
|
||||||
file' (ctf/update-file-data file #(cfcp/fix-missing-swap-slots % {}))]
|
|
||||||
|
|
||||||
(t/is (empty? (d/map-diff file file')))))
|
|
||||||
|
|
||||||
(t/testing "file with a normally swapped copy should not need any action"
|
|
||||||
(let [file
|
|
||||||
;; {:main1-root} [:name Frame1] # [Component :component1]
|
|
||||||
;; :main1-child [:name Rect1]
|
|
||||||
;;
|
|
||||||
;; {:main2-root} [:name Frame2] # [Component :component2]
|
|
||||||
;; :nested-head [:name Frame1] @--> [Component :component1] :main1-root
|
|
||||||
;; <no-label> [:name Rect1] ---> :main1-child
|
|
||||||
;;
|
|
||||||
;; {:main3-root} [:name Frame3] # [Component :component3]
|
|
||||||
;; :main3-child [:name Rect3]
|
|
||||||
;;
|
|
||||||
;; :copy2-root [:name Frame2] #--> [Component :component2] :main2-root
|
|
||||||
;; :copy2-nested-head [:name Frame3] @--> [Component :component3] :main3-root
|
|
||||||
;; {swap-slot :nested-head}
|
|
||||||
;; <no-label> [:name Rect3] ---> :main3-child
|
|
||||||
(-> (thf/sample-file :file1)
|
|
||||||
(tho/add-nested-component :component1 :main1-root :main1-child
|
|
||||||
:component2 :main2-root :nested-head)
|
|
||||||
(thc/instantiate-component :component2 :copy2-root :children-labels [:copy2-nested-head])
|
|
||||||
(tho/add-simple-component :component3 :main3-root :main3-child
|
|
||||||
:root-params {:name "Frame3"}
|
|
||||||
:child-params {:name "Rect3"})
|
|
||||||
(tho/swap-component-in-first-child :copy2-root :component3))
|
|
||||||
|
|
||||||
file' (ctf/update-file-data file #(cfcp/fix-missing-swap-slots % {}))]
|
|
||||||
|
|
||||||
(t/is (empty? (d/map-diff file file')))))
|
|
||||||
|
|
||||||
(t/testing "file with a swapped nested copy in a main should not need any action"
|
|
||||||
(let [file
|
|
||||||
;; {:main1-root} [:name Frame1] # [Component :component1]
|
|
||||||
;; :main1-child [:name Rect1]
|
|
||||||
;;
|
|
||||||
;; {:main3-root} [:name Frame3] # [Component :component3]
|
|
||||||
;; :main3-child [:name Rect3]
|
|
||||||
;;
|
|
||||||
;; {:main2-root} [:name Frame2] # [Component :component2]
|
|
||||||
;; :nested-head [:name Frame3] @--> [Component :component3] :main3-root
|
|
||||||
;; {swap-slot :nested-head}
|
|
||||||
;; <no-label> [:name Rect3] ---> :main3-child
|
|
||||||
;;
|
|
||||||
;; :copy2-root [:name Frame2] #--> [Component :component2] :main2-root
|
|
||||||
;; :copy2-nested-head [:name Frame3] @--> [Component :component3] :nested-head
|
|
||||||
;; <no-label> [:name Rect3] ---> <no-label>
|
|
||||||
(-> (thf/sample-file :file1)
|
|
||||||
(tho/add-nested-component :component1 :main1-root :main1-child
|
|
||||||
:component2 :main2-root :nested-head)
|
|
||||||
(thc/instantiate-component :component2 :copy2-root :children-labels [:copy2-nested-head])
|
|
||||||
(tho/add-simple-component :component3 :main3-root :main3-child
|
|
||||||
:root-params {:name "Frame3"}
|
|
||||||
:child-params {:name "Rect3"})
|
|
||||||
(tho/swap-component-in-shape :nested-head :component3
|
|
||||||
:propagate-fn #(tho/propagate-component-changes % :component2)))
|
|
||||||
|
|
||||||
file' (ctf/update-file-data file #(cfcp/fix-missing-swap-slots % {}))]
|
|
||||||
|
|
||||||
(t/is (empty? (d/map-diff file file')))))
|
|
||||||
|
|
||||||
(t/testing "file with a swapped copy with broken slot should have it repaired"
|
|
||||||
(let [file
|
|
||||||
;; {:main1-root} [:name Frame1] # [Component :component1]
|
|
||||||
;; :main1-child [:name Rect1]
|
|
||||||
;;
|
|
||||||
;; {:main2-root} [:name Frame2] # [Component :component2]
|
|
||||||
;; :nested-head [:name Frame1] @--> [Component :component1] :main1-root
|
|
||||||
;; <no-label> [:name Rect1] ---> :main1-child
|
|
||||||
;;
|
|
||||||
;; {:main3-root} [:name Frame3] # [Component :component3]
|
|
||||||
;; :main3-child [:name Rect3]
|
|
||||||
;;
|
|
||||||
;; :copy2-root [:name Frame2] #--> [Component :component2] :main2-root
|
|
||||||
;; :copy2-nested-head [:name Frame3] @--> [Component :component3] :main3-root
|
|
||||||
;; NO SWAP SLOT
|
|
||||||
;; <no-label> [:name Rect3] ---> :main3-child
|
|
||||||
(-> (thf/sample-file :file1)
|
|
||||||
(tho/add-nested-component :component1 :main1-root :main1-child
|
|
||||||
:component2 :main2-root :nested-head)
|
|
||||||
(thc/instantiate-component :component2 :copy2-root :children-labels [:copy2-nested-head])
|
|
||||||
(tho/add-simple-component :component3 :main3-root :main3-child
|
|
||||||
:root-params {:name "Frame3"}
|
|
||||||
:child-params {:name "Rect3"})
|
|
||||||
(tho/swap-component-in-first-child :copy2-root :component3)
|
|
||||||
(ths/update-shape :copy2-nested-head :touched nil))
|
|
||||||
|
|
||||||
file' (ctf/update-file-data file #(cfcp/fix-missing-swap-slots % {}))
|
|
||||||
|
|
||||||
diff (d/map-diff file file')
|
|
||||||
|
|
||||||
expected-diff {:data
|
|
||||||
{:pages-index
|
|
||||||
{(thf/current-page-id file)
|
|
||||||
{:objects
|
|
||||||
{(thi/id :copy2-nested-head)
|
|
||||||
{:touched
|
|
||||||
[nil
|
|
||||||
#{(ctk/build-swap-slot-group (str (thi/id :nested-head)))}]}}}}}}]
|
|
||||||
|
|
||||||
(t/is (= expected-diff diff))))
|
|
||||||
|
|
||||||
(t/testing "file with a swapped copy inside a main with broken slot has no effect since it cannot be distinguished"
|
|
||||||
(let [file
|
|
||||||
;; {:main1-root} [:name Frame1] # [Component :component1]
|
|
||||||
;; :main1-child [:name Rect1]
|
|
||||||
;;
|
|
||||||
;; {:main3-root} [:name Frame3] # [Component :component3]
|
|
||||||
;; :main3-child [:name Rect3]
|
|
||||||
;;
|
|
||||||
;; {:main2-root} [:name Frame2] # [Component :component2]
|
|
||||||
;; :nested-head [:name Frame3] @--> [Component :component3] :main3-root
|
|
||||||
;; NO SWAP SLOT
|
|
||||||
;; <no-label> [:name Rect3] ---> :main3-child
|
|
||||||
;;
|
|
||||||
;; :copy2-root [:name Frame2] #--> [Component :component2] :main2-root
|
|
||||||
;; :copy2-nested-head [:name Frame3] @--> [Component :component3] :nested-head
|
|
||||||
;; <no-label> [:name Rect3] ---> <no-label>
|
|
||||||
(-> (thf/sample-file :file1)
|
|
||||||
(tho/add-nested-component :component1 :main1-root :main1-child
|
|
||||||
:component2 :main2-root :nested-head)
|
|
||||||
(thc/instantiate-component :component2 :copy2-root :children-labels [:copy2-nested-head])
|
|
||||||
(tho/add-simple-component :component3 :main3-root :main3-child
|
|
||||||
:root-params {:name "Frame3"}
|
|
||||||
:child-params {:name "Rect3"})
|
|
||||||
(tho/swap-component-in-shape :nested-head :component3
|
|
||||||
:propagate-fn #(tho/propagate-component-changes % :component2))
|
|
||||||
(ths/update-shape :nested-head :touched nil))
|
|
||||||
|
|
||||||
file' (ctf/update-file-data file #(cfcp/fix-missing-swap-slots % {}))]
|
|
||||||
|
|
||||||
(t/is (empty? (d/map-diff file file')))))
|
|
||||||
|
|
||||||
(t/testing "file with a two levels nested copy in a main swapped with broken slot should have it repaired"
|
|
||||||
(let [file
|
|
||||||
;; {:main1-root} [:name Frame1] # [Component :component1]
|
|
||||||
;; :main1-child [:name Rect1]
|
|
||||||
;;
|
|
||||||
;; {:main2-root} [:name Frame2] # [Component :component2]
|
|
||||||
;; :nested-head1 [:name Frame1] @--> [Component :component1] :main1-root
|
|
||||||
;; <no-label> [:name Rect1] ---> :main1-child
|
|
||||||
;;
|
|
||||||
;; {:main4-root} [:name Frame4] # [Component :component4]
|
|
||||||
;; :main4-child [:name Rect4]
|
|
||||||
;;
|
|
||||||
;; {:main3-root} [:name Frame3] # [Component :component3]
|
|
||||||
;; :nested-head2 [:name Frame2] @--> [Component :component2] :main2-root
|
|
||||||
;; :nested-subhead2 [:name Frame4] @--> [Component :component4] :main4-root
|
|
||||||
;; NO SWAP SLOT
|
|
||||||
;; <no-label> [:name Rect4] ---> :main4-child
|
|
||||||
;;
|
|
||||||
;; :copy2-root [:name Frame3] #--> [Component :component3] :main3-root
|
|
||||||
;; <no-label> [:name Frame2] @--> [Component :component2] :nested-head2
|
|
||||||
;; <no-label> [:name Frame4] @--> [Component :component4] :nested-subhead2
|
|
||||||
;; <no-label> [:name Rect4] ---> <no-label>
|
|
||||||
(-> (thf/sample-file :file1)
|
|
||||||
(tho/add-two-levels-nested-component-with-copy :component1 :main1-root :main1-child
|
|
||||||
:component2 :main2-root :nested-head1
|
|
||||||
:component3 :main3-root :nested-head2 :nested-subhead2
|
|
||||||
:copy2-root)
|
|
||||||
(tho/add-simple-component :component4 :main4-root :main4-child
|
|
||||||
:root-params {:name "Frame4"}
|
|
||||||
:child-params {:name "Rect4"})
|
|
||||||
(tho/swap-component-in-shape :nested-subhead2 :component4
|
|
||||||
:propagate-fn #(tho/propagate-component-changes % :component3))
|
|
||||||
(ths/update-shape :nested-subhead2 :touched nil))
|
|
||||||
|
|
||||||
file' (ctf/update-file-data file #(cfcp/fix-missing-swap-slots % {}))
|
|
||||||
|
|
||||||
diff (d/map-diff file file')
|
|
||||||
|
|
||||||
expected-diff {:data
|
|
||||||
{:pages-index
|
|
||||||
{(thf/current-page-id file)
|
|
||||||
{:objects
|
|
||||||
{(thi/id :nested-subhead2)
|
|
||||||
{:touched
|
|
||||||
[nil
|
|
||||||
#{(ctk/build-swap-slot-group (str (thi/id :nested-head1)))}]}}}}}}]
|
|
||||||
|
|
||||||
(t/is (= expected-diff diff))))
|
|
||||||
|
|
||||||
(t/testing "when components are in external libraries, the fix still works well"
|
|
||||||
(let [library1
|
|
||||||
;; {:main1-root} [:name Frame1] # [Component :component1]
|
|
||||||
;; :main1-child [:name Rect1]
|
|
||||||
;;
|
|
||||||
;; {:main2-root} [:name Frame2] # [Component :component2]
|
|
||||||
;; :nested2-head [:name Frame1] @--> [Component :component1] :main1-root
|
|
||||||
;; :nested2-child [:name Rect1] ---> :main1-child
|
|
||||||
(-> (thf/sample-file :library1)
|
|
||||||
(tho/add-nested-component :component1 :main1-root :main1-child
|
|
||||||
:component2 :main2-root :nested2-head
|
|
||||||
:nested-head-params {:children-labels [:nested2-child]}))
|
|
||||||
library2
|
|
||||||
;; {:main3-root} [:name Frame3] # [Component :component3]
|
|
||||||
;; :main3-child [:name Rect3]
|
|
||||||
;;
|
|
||||||
;; {:main4-root} [:name Frame4] # [Component :component4]
|
|
||||||
;; :nested4-head [:name Frame3] @--> [Component :component1] :main3-root
|
|
||||||
;; :nested4-child [:name Rect3] ---> :main3-child
|
|
||||||
(-> (thf/sample-file :library2)
|
|
||||||
(tho/add-nested-component :component3 :main3-root :main3-child
|
|
||||||
:component4 :main4-root :nested4-head
|
|
||||||
:root1-params {:name "Frame3"}
|
|
||||||
:main1-child-params {:name "Rect3"}
|
|
||||||
:main2-root-params {:name "Frame4"}
|
|
||||||
:nested-head-params {:children-labels [:nested4-child]}))
|
|
||||||
|
|
||||||
file
|
|
||||||
;; :copy2 [:name Frame2] #--> [Component :component2] :main2-root
|
|
||||||
;; :copy2-nested-head [:name Frame4] @--> [Component :component4] :main4-root
|
|
||||||
;; NO SWAP SLOT
|
|
||||||
;; <no-label> [:name Frame3] @--> :nested4-head
|
|
||||||
;; <no-label> [:name Rect3] ---> :nested4-child
|
|
||||||
(-> (thf/sample-file :file1)
|
|
||||||
(thc/instantiate-component :component2 :copy2 :children-labels [:copy2-nested-head]
|
|
||||||
:library library1)
|
|
||||||
(tho/swap-component-in-first-child :copy2 :component4 :library library2)
|
|
||||||
(ths/update-shape :copy2-nested-head :touched nil))
|
|
||||||
|
|
||||||
libraries {(:id library1) library1
|
|
||||||
(:id library2) library2}
|
|
||||||
|
|
||||||
file' (ctf/update-file-data file #(cfcp/fix-missing-swap-slots % libraries))
|
|
||||||
|
|
||||||
diff (d/map-diff file file')
|
|
||||||
|
|
||||||
expected-diff {:data
|
|
||||||
{:pages-index
|
|
||||||
{(thf/current-page-id file)
|
|
||||||
{:objects
|
|
||||||
{(thi/id :copy2-nested-head)
|
|
||||||
{:touched
|
|
||||||
[nil
|
|
||||||
#{(ctk/build-swap-slot-group (str (thi/id :nested2-head)))}]}}}}}}]
|
|
||||||
|
|
||||||
(t/is (= expected-diff diff)))))
|
|
||||||
|
|
||||||
(t/deftest test-sync-component-id-with-ref-shape
|
|
||||||
|
|
||||||
(t/testing "nil file should return nil"
|
|
||||||
(let [file nil
|
|
||||||
file' (ctf/update-file-data file #(cfcp/sync-component-id-with-ref-shape % {}))]
|
|
||||||
(t/is (nil? file'))))
|
|
||||||
|
|
||||||
(t/testing "empty file should not need any action"
|
|
||||||
(let [file (thf/sample-file :file1)
|
|
||||||
file' (ctf/update-file-data file #(cfcp/sync-component-id-with-ref-shape % {}))]
|
|
||||||
(t/is (empty? (d/map-diff file file')))))
|
|
||||||
|
|
||||||
(t/testing "file without components should not need any action"
|
|
||||||
(let [file
|
|
||||||
;; :frame1 [:name Frame1]
|
|
||||||
;; :child1 [:name Rect1]
|
|
||||||
(-> (thf/sample-file :file1)
|
|
||||||
(tho/add-frame-with-child :frame1 :shape1))
|
|
||||||
|
|
||||||
file' (ctf/update-file-data file #(cfcp/sync-component-id-with-ref-shape % {}))]
|
|
||||||
|
|
||||||
(t/is (empty? (d/map-diff file file')))))
|
|
||||||
|
|
||||||
(t/testing "file with valid normal components should not need any action"
|
|
||||||
(let [file
|
|
||||||
;; {:main1-root} [:name Frame1] # [Component :component1]
|
|
||||||
;; :main1-child [:name Rect1]
|
|
||||||
;;
|
|
||||||
;; {:main2-root} [:name Frame2] # [Component :component2]
|
|
||||||
;; :nested-head1 [:name Frame1] @--> [Component :component1] :main1-root
|
|
||||||
;; <no-label> [:name Rect1] ---> :main1-child
|
|
||||||
;;
|
|
||||||
;; {:main3-root} [:name Frame3] # [Component :component3]
|
|
||||||
;; :nested-head2 [:name Frame2] @--> [Component :component2] :main2-root
|
|
||||||
;; :nested-subhead2 [:name Frame1] @--> [Component :component1] :nested-head1
|
|
||||||
;; <no-label> [:name Rect1] ---> <no-label>
|
|
||||||
;;
|
|
||||||
;; :copy2-root [:name Frame3] #--> [Component :component3] :main3-root
|
|
||||||
;; <no-label> [:name Frame2] @--> [Component :component2] :nested-head2
|
|
||||||
;; <no-label> [:name Frame1] @--> [Component :component1] :nested-subhead2
|
|
||||||
;; <no-label> [:name Rect1] ---> <no-label>
|
|
||||||
(-> (thf/sample-file :file1)
|
|
||||||
(tho/add-two-levels-nested-component-with-copy :component1 :main1-root :main1-child
|
|
||||||
:component2 :main2-root :nested-head1
|
|
||||||
:component3 :main3-root :nested-head2 :nested-subhead2
|
|
||||||
:copy2-root))
|
|
||||||
|
|
||||||
file' (ctf/update-file-data file #(cfcp/sync-component-id-with-ref-shape % {}))]
|
|
||||||
|
|
||||||
#_(thf/dump-file file') ;; Uncomment to debug
|
|
||||||
(t/is (empty? (d/map-diff file file')))))
|
|
||||||
|
|
||||||
(t/testing "file with valid swapped components should not need any action"
|
|
||||||
(let [file
|
|
||||||
;; {:main1-root} [:name Frame1] # [Component :component1]
|
|
||||||
;; :main1-child [:name Rect1]
|
|
||||||
;;
|
|
||||||
;; {:main2-root} [:name Frame2] # [Component :component2]
|
|
||||||
;; :nested-head [:name Frame1] @--> [Component :component1] :main1-root
|
|
||||||
;; <no-label> [:name Rect1] ---> :main1-child
|
|
||||||
;;
|
|
||||||
;; {:main3-root} [:name Frame3] # [Component :component3]
|
|
||||||
;; :main3-child [:name Rect3]
|
|
||||||
;;
|
|
||||||
;; :copy2-root [:name Frame2] #--> [Component :component2] :main2-root
|
|
||||||
;; <no-label> [:name Frame1] @--> [Component :component1] :nested-head
|
|
||||||
;; <no-label> [:name Rect1] ---> <no-label>
|
|
||||||
;;
|
|
||||||
;; :copy3-root [:name Frame2] #--> [Component :component2] :main2-root
|
|
||||||
;; :copy3-nested-head [:name Frame3] @--> [Component :component3] :main3-root
|
|
||||||
;; {swap-slot :nested-head}
|
|
||||||
;; <no-label> [:name Rect3] ---> :main3-child
|
|
||||||
(-> (thf/sample-file :file1)
|
|
||||||
(tho/add-nested-component-with-copy :component1 :main1-root :main1-child
|
|
||||||
:component2 :main2-root :nested-head
|
|
||||||
:copy2-root)
|
|
||||||
(tho/add-simple-component :component3 :main3-root :main3-child
|
|
||||||
:root-params {:name "Frame3"}
|
|
||||||
:child-params {:name "Rect3"})
|
|
||||||
(thc/instantiate-component :component2 :copy3-root :children-labels [:copy3-nested-head])
|
|
||||||
(tho/swap-component-in-first-child :copy3-root :component3))
|
|
||||||
|
|
||||||
file' (ctf/update-file-data file #(cfcp/sync-component-id-with-ref-shape % {}))]
|
|
||||||
|
|
||||||
#_(thf/dump-file file') ;; Uncomment to debug
|
|
||||||
(t/is (empty? (d/map-diff file file')))))
|
|
||||||
|
|
||||||
(t/testing "file with a non swapped copy with broken component id/file should have it repaired"
|
|
||||||
(let [file
|
|
||||||
;; {:main1-root} [:name Frame1] # [Component :component1]
|
|
||||||
;; :main1-child [:name Rect1]
|
|
||||||
;;
|
|
||||||
;; {:main2-root} [:name Frame2] # [Component :component2]
|
|
||||||
;; :nested-head [:name Frame1] @--> [Component :component1] :main1-root
|
|
||||||
;; <no-label> [:name Rect1] ---> :main1-child
|
|
||||||
;;
|
|
||||||
;; :copy2-root [:name Frame2] #--> [Component :component2] :main2-root
|
|
||||||
;; :copy2-nested-head [:name Frame1] @--> [Component <bad>] :nested-head ## <- BAD component-id
|
|
||||||
;; <no-label> [:name Rect1] ---> <no-label>
|
|
||||||
;;
|
|
||||||
;; :copy3-root [:name Frame2] #--> [Component :component2] :main2-root
|
|
||||||
;; :copy3-nested-head [:name Frame1] @--> [Component <bad>] :nested-head ## <- BAD component-file
|
|
||||||
;; <no-label> [:name Rect1] ---> <no-label>
|
|
||||||
(-> (thf/sample-file :file1)
|
|
||||||
(tho/add-nested-component :component1 :main1-root :main1-child
|
|
||||||
:component2 :main2-root :nested-head)
|
|
||||||
(thc/instantiate-component :component2 :copy2-root :children-labels [:copy2-nested-head])
|
|
||||||
(thc/instantiate-component :component2 :copy3-root :children-labels [:copy3-nested-head])
|
|
||||||
(ths/update-shape :copy2-nested-head :component-id (thi/new-id! :some-other-id))
|
|
||||||
(ths/update-shape :copy3-nested-head :component-file (thi/new-id! :some-other-file)))
|
|
||||||
|
|
||||||
file' (ctf/update-file-data file #(cfcp/sync-component-id-with-ref-shape % {}))
|
|
||||||
|
|
||||||
diff (d/map-diff file file')
|
|
||||||
|
|
||||||
expected-diff {:data
|
|
||||||
{:pages-index
|
|
||||||
{(thf/current-page-id file)
|
|
||||||
{:objects
|
|
||||||
{(thi/id :copy2-nested-head)
|
|
||||||
{:component-id
|
|
||||||
[(thi/id :some-other-id) (thi/id :component1)]}
|
|
||||||
(thi/id :copy3-nested-head)
|
|
||||||
{:component-file
|
|
||||||
[(thi/id :some-other-file) (thi/id :file1)]}}}}}}]
|
|
||||||
|
|
||||||
#_(ctf/dump-tree file' (thf/current-page-id file') {(:id file') file'} {:show-ids true}) ;; Uncomment to debug
|
|
||||||
(t/is (= expected-diff diff))))
|
|
||||||
|
|
||||||
(t/testing "file with a copy of a swapped main with broken component id/file should have it repaired"
|
|
||||||
(let [file
|
|
||||||
;; {:main1-root} [:name Frame1] # [Component :component1]
|
|
||||||
;; :main1-child [:name Rect1]
|
|
||||||
;;
|
|
||||||
;; {:main3-root} [:name Frame3] # [Component :component3]
|
|
||||||
;; :main3-child [:name Rect3]
|
|
||||||
;;
|
|
||||||
;; {:main2-root} [:name Frame2] # [Component :component2]
|
|
||||||
;; :nested-head [:name Frame3] @--> [Component :component3] :main3-root
|
|
||||||
;; {swap-slot :nested-head}
|
|
||||||
;; <no-label> [:name Rect3] ---> :main3-child
|
|
||||||
;;
|
|
||||||
;; :copy2-root [:name Frame2] #--> [Component :component2] :main2-root
|
|
||||||
;; :copy2-nested-head [:name Frame3] @--> [Component: <bad>] :nested-head ## <- BAD component-id/file
|
|
||||||
;; <no-label> [:name Rect3] ---> <no-label>
|
|
||||||
(-> (thf/sample-file :file1)
|
|
||||||
(tho/add-nested-component :component1 :main1-root :main1-child
|
|
||||||
:component2 :main2-root :nested-head)
|
|
||||||
(thc/instantiate-component :component2 :copy2-root :children-labels [:copy2-nested-head])
|
|
||||||
(tho/add-simple-component :component3 :main3-root :main3-child
|
|
||||||
:root-params {:name "Frame3"}
|
|
||||||
:child-params {:name "Rect3"})
|
|
||||||
(tho/swap-component-in-shape :nested-head :component3
|
|
||||||
:propagate-fn #(tho/propagate-component-changes % :component2))
|
|
||||||
(ths/update-shape :copy2-nested-head :component-id (thi/new-id! :some-other-id))
|
|
||||||
(ths/update-shape :copy2-nested-head :component-file (thi/new-id! :some-other-file)))
|
|
||||||
|
|
||||||
file' (ctf/update-file-data file #(cfcp/sync-component-id-with-ref-shape % {}))
|
|
||||||
|
|
||||||
diff (d/map-diff file file')
|
|
||||||
|
|
||||||
expected-diff {:data
|
|
||||||
{:pages-index
|
|
||||||
{(thf/current-page-id file)
|
|
||||||
{:objects
|
|
||||||
{(thi/id :copy2-nested-head)
|
|
||||||
{:component-id
|
|
||||||
[(thi/id :some-other-id) (thi/id :component3)]
|
|
||||||
:component-file
|
|
||||||
[(thi/id :some-other-file) (thi/id :file1)]}}}}}}]
|
|
||||||
|
|
||||||
#_(ctf/dump-tree file' (thf/current-page-id file') {(:id file') file'} {:show-ids true}) ;; Uncomment to debug
|
|
||||||
(t/is (= expected-diff diff))))
|
|
||||||
|
|
||||||
(t/testing "file with multiple copies of same component should sync all"
|
|
||||||
(let [file
|
|
||||||
(-> (thf/sample-file :file1)
|
|
||||||
(tho/add-simple-component :component1 :frame1 :shape1)
|
|
||||||
(thc/instantiate-component :component1 :copy1-root :children-labels [:copy1-child])
|
|
||||||
(thc/instantiate-component :component1 :copy2-root :children-labels [:copy2-child])
|
|
||||||
(ths/update-shape :copy1-child :component-id (thi/new-id! :wrong-id1))
|
|
||||||
(ths/update-shape :copy2-child :component-id (thi/new-id! :wrong-id2)))
|
|
||||||
|
|
||||||
file' (ctf/update-file-data file #(cfcp/sync-component-id-with-ref-shape % {}))
|
|
||||||
|
|
||||||
diff (d/map-diff file file')]
|
|
||||||
|
|
||||||
;; Both copies should be corrected
|
|
||||||
(t/is (contains? diff :data))
|
|
||||||
(t/is (contains? (get-in diff [:data :pages-index]) (thf/current-page-id file)))))
|
|
||||||
|
|
||||||
(t/testing "file with a copy root with broken component id/file cannot be repaired. But it's propagated to copies."
|
|
||||||
(let [file
|
|
||||||
;; {:main1-root} [:name Frame1] # [Component :component1]
|
|
||||||
;; :main1-child [:name Rect1]
|
|
||||||
;;
|
|
||||||
;; {:main2-root} [:name Frame2] # [Component :component2]
|
|
||||||
;; :nested-head [:name Frame1] @--> [Component <bad>] :main1-root ## <- BAD component-id/file
|
|
||||||
;; <no-label> [:name Rect1] ---> :main1-child
|
|
||||||
;;
|
|
||||||
;; :copy2-root [:name Frame2] #--> [Component :component2] :main2-root
|
|
||||||
;; :copy2-nested-head [:name Frame1] @--> [Component :component1] :nested-head
|
|
||||||
;; <no-label> [:name Rect1] ---> <no-label>
|
|
||||||
(-> (thf/sample-file :file1)
|
|
||||||
(tho/add-nested-component :component1 :main1-root :main1-child
|
|
||||||
:component2 :main2-root :nested-head)
|
|
||||||
(thc/instantiate-component :component2 :copy2-root :children-labels [:copy2-nested-head])
|
|
||||||
(ths/update-shape :nested-head :component-id (thi/new-id! :some-other-id))
|
|
||||||
(ths/update-shape :nested-head :component-file (thi/new-id! :some-other-file)))
|
|
||||||
|
|
||||||
file' (ctf/update-file-data file #(cfcp/sync-component-id-with-ref-shape % {}))
|
|
||||||
|
|
||||||
diff (d/map-diff file file')
|
|
||||||
|
|
||||||
expected-diff {:data
|
|
||||||
{:pages-index
|
|
||||||
{(thf/current-page-id file)
|
|
||||||
{:objects
|
|
||||||
{(thi/id :copy2-nested-head)
|
|
||||||
{:component-id
|
|
||||||
[(thi/id :component1) (thi/id :some-other-id)]
|
|
||||||
:component-file
|
|
||||||
[(thi/id :file1) (thi/id :some-other-file)]}}}}}}]
|
|
||||||
|
|
||||||
(t/is (= expected-diff diff))))
|
|
||||||
|
|
||||||
(t/testing "file with a 2nd nested copy inside a main with broken component/id should have it repaired, and propagated to copies"
|
|
||||||
(let [file
|
|
||||||
;; {:main1-root} [:name Frame1] # [Component :component1]
|
|
||||||
;; :main1-child [:name Rect1]
|
|
||||||
;;
|
|
||||||
;; {:main2-root} [:name Frame2] # [Component :component2]
|
|
||||||
;; :nested-head1 [:name Frame1] @--> [Component :component1] :main1-root
|
|
||||||
;; <no-label> [:name Rect1] ---> :main1-child
|
|
||||||
;;
|
|
||||||
;; {:main3-root} [:name Frame3] # [Component :component3]
|
|
||||||
;; :nested-head2 [:name Frame2] @--> [Component :component2] :main2-root
|
|
||||||
;; :nested-subhead2 [:name Frame1] @--> [Component <bad>] :nested-head1 ## <- BAD component-id/file
|
|
||||||
;; <no-label> [:name Rect1] ---> <no-label>
|
|
||||||
;;
|
|
||||||
;; :copy2-root [:name Frame3] #--> [Component :component3] :main3-root
|
|
||||||
;; <no-label> [:name Frame2] @--> [Component :component2] :nested-head2
|
|
||||||
;; <no-label> [:name Frame1] @--> [Component :component1] :nested-subhead2
|
|
||||||
;; <no-label> [:name Rect1] ---> <no-label>
|
|
||||||
(-> (thf/sample-file :file1)
|
|
||||||
(tho/add-two-levels-nested-component-with-copy :component1 :main1-root :main1-child
|
|
||||||
:component2 :main2-root :nested-head1
|
|
||||||
:component3 :main3-root :nested-head2 :nested-subhead2
|
|
||||||
:copy2-root)
|
|
||||||
(ths/update-shape :nested-subhead2 :component-id (thi/new-id! :some-other-id))
|
|
||||||
(ths/update-shape :nested-subhead2 :component-file (thi/new-id! :some-other-file)))
|
|
||||||
|
|
||||||
copy2-root (ths/get-shape file :copy2-root)
|
|
||||||
copy2-root-child1 (ths/get-shape-by-id file (first (:shapes copy2-root)))
|
|
||||||
copy2-root-child2 (ths/get-shape-by-id file (first (:shapes copy2-root-child1)))
|
|
||||||
file (-> file
|
|
||||||
(ths/update-shape-by-id (:id copy2-root-child2) :component-id (thi/id :some-other-id))
|
|
||||||
(ths/update-shape-by-id (:id copy2-root-child2) :component-file (thi/id :some-other-file)))
|
|
||||||
|
|
||||||
file' (ctf/update-file-data file #(cfcp/sync-component-id-with-ref-shape % {}))
|
|
||||||
|
|
||||||
diff (d/map-diff file file')
|
|
||||||
|
|
||||||
expected-diff {:data
|
|
||||||
{:pages-index
|
|
||||||
{(thf/current-page-id file)
|
|
||||||
{:objects
|
|
||||||
{(thi/id :nested-subhead2)
|
|
||||||
{:component-id
|
|
||||||
[(thi/id :some-other-id) (thi/id :component1)]
|
|
||||||
:component-file
|
|
||||||
[(thi/id :some-other-file) (thi/id :file1)]}
|
|
||||||
(:id copy2-root-child2)
|
|
||||||
{:component-id
|
|
||||||
[(thi/id :some-other-id) (thi/id :component1)]
|
|
||||||
:component-file
|
|
||||||
[(thi/id :some-other-file) (thi/id :file1)]}}}}}}]
|
|
||||||
|
|
||||||
#_(ctf/dump-tree file' (thf/current-page-id file') {(:id file') file'} {:show-ids true}) ;; Uncomment to debug
|
|
||||||
(t/is (= expected-diff diff))))
|
|
||||||
|
|
||||||
(t/testing "when components are in external libraries, the fix still works well"
|
|
||||||
(let [library1
|
|
||||||
;; {:main1-root} [:name Frame1] # [Component :component1]
|
|
||||||
;; :main1-child [:name Rect1]
|
|
||||||
;;
|
|
||||||
;; {:main2-root} [:name Frame2] # [Component :component2]
|
|
||||||
;; :nested2-head [:name Frame4] @--> [Component :component4] :main4-root
|
|
||||||
;; {swap-slot :nested2-head}
|
|
||||||
;; :nested4-head [:name Frame3] @--> [Component: component3] :main3-root
|
|
||||||
;; :nested4-child [:name Rect3] ---> :nested4-child
|
|
||||||
(-> (thf/sample-file :library1)
|
|
||||||
(tho/add-nested-component :component1 :main1-root :main1-child
|
|
||||||
:component2 :main2-root :nested2-head
|
|
||||||
:nested-head-params {:children-labels [:nested2-child]}))
|
|
||||||
library2
|
|
||||||
;; {:main3-root} [:name Frame3] # [Component :component3]
|
|
||||||
;; :main3-child [:name Rect3]
|
|
||||||
;;
|
|
||||||
;; {:main4-root} [:name Frame4] # [Component :component4]
|
|
||||||
;; :nested4-head [:name Frame3] @--> [Component :component1] :main3-root
|
|
||||||
;; :nested4-child [:name Rect3] ---> :main3-child
|
|
||||||
(-> (thf/sample-file :library2)
|
|
||||||
(tho/add-nested-component :component3 :main3-root :main3-child
|
|
||||||
:component4 :main4-root :nested4-head
|
|
||||||
:root1-params {:name "Frame3"}
|
|
||||||
:main1-child-params {:name "Rect3"}
|
|
||||||
:main2-root-params {:name "Frame4"}
|
|
||||||
:nested-head-params {:children-labels [:nested4-child]}))
|
|
||||||
|
|
||||||
library1
|
|
||||||
(tho/swap-component-in-shape library1 :nested2-head :component4 :library library2)
|
|
||||||
|
|
||||||
file
|
|
||||||
;; :copy2 [:name Frame2] #--> [Component :component2] :main2-root
|
|
||||||
;; :copy2-nested-head [:name Frame4] @--> [Component <bad>] :main4-root ## <- BAD component-id/file
|
|
||||||
;; <no-label> [:name Frame3] @--> :nested4-head
|
|
||||||
;; <no-label> [:name Rect3] ---> :nested4-child
|
|
||||||
(-> (thf/sample-file :file1)
|
|
||||||
(thc/instantiate-component :component2 :copy2 :children-labels [:copy2-nested-head]
|
|
||||||
:library library1)
|
|
||||||
(ths/update-shape :copy2-nested-head :component-id (thi/new-id! :some-other-id))
|
|
||||||
(ths/update-shape :copy2-nested-head :component-file (thi/new-id! :some-other-file)))
|
|
||||||
|
|
||||||
libraries {(:id library1) library1
|
|
||||||
(:id library2) library2}
|
|
||||||
|
|
||||||
file' (ctf/update-file-data file #(cfcp/sync-component-id-with-ref-shape % libraries))
|
|
||||||
|
|
||||||
diff (d/map-diff file file')
|
|
||||||
|
|
||||||
expected-diff {:data
|
|
||||||
{:pages-index
|
|
||||||
{(thf/current-page-id file)
|
|
||||||
{:objects
|
|
||||||
{(thi/id :copy2-nested-head)
|
|
||||||
{:component-id
|
|
||||||
[(thi/id :some-other-id) (thi/id :component4)]
|
|
||||||
:component-file
|
|
||||||
[(thi/id :some-other-file) (thi/id :library2)]}}}}}}]
|
|
||||||
|
|
||||||
#_(thf/dump-file library2) ;; Uncomment to debug
|
|
||||||
(t/is (= expected-diff diff))))
|
|
||||||
|
|
||||||
(t/testing "file with several broken ids should propagate to all copies"
|
|
||||||
(let [file
|
|
||||||
;; {:main1-root} [:name Frame1] # [Component :component1]
|
|
||||||
;; :main1-child [:name Rect1]
|
|
||||||
;;
|
|
||||||
;; {:main2-root} [:name Frame2] # [Component :component2]
|
|
||||||
;; :nested-head1 [:name Frame1] @--> [Component :component1] :main1-root
|
|
||||||
;; <no-label> [:name Rect1] ---> :main1-child
|
|
||||||
;;
|
|
||||||
;; {:main3-root} [:name Frame3] # [Component :component3]
|
|
||||||
;; :nested-head2 [:name Frame2] @--> [Component <bad>] :main2-root ## <- BAD component-id
|
|
||||||
;; :nested-subhead2 [:name Frame1] @--> [Component <bad>] :nested-head1 ## <- BAD component-id
|
|
||||||
;; <no-label> [:name Rect1] ---> <no-label>
|
|
||||||
;;
|
|
||||||
;; :copy2-root [:name Frame3] #--> [Component :component3] :main3-root
|
|
||||||
;; <no-label> [:name Frame2] @--> [Component :component2] :nested-head2
|
|
||||||
;; <no-label> [:name Frame1] @--> [Component :component1] :nested-subhead2
|
|
||||||
;; <no-label> [:name Rect1] ---> <no-label>
|
|
||||||
(-> (thf/sample-file :file1)
|
|
||||||
(tho/add-two-levels-nested-component-with-copy :component1 :main1-root :main1-child
|
|
||||||
:component2 :main2-root :nested-head1
|
|
||||||
:component3 :main3-root :nested-head2 :nested-subhead2
|
|
||||||
:copy2-root)
|
|
||||||
;; Corrupt both levels
|
|
||||||
(ths/update-shape :nested-head2 :component-id (thi/new-id! :wrong-comp2))
|
|
||||||
(ths/update-shape :nested-subhead2 :component-id (thi/new-id! :wrong-comp3)))
|
|
||||||
|
|
||||||
file' (ctf/update-file-data file #(cfcp/sync-component-id-with-ref-shape % {}))
|
|
||||||
copy2-root (ths/get-shape file' :copy2-root)
|
|
||||||
copy2-root-child1 (ths/get-shape-by-id file' (first (:shapes copy2-root)))
|
|
||||||
copy2-root-child2 (ths/get-shape-by-id file' (first (:shapes copy2-root-child1)))
|
|
||||||
|
|
||||||
diff (d/map-diff file file')
|
|
||||||
|
|
||||||
expected-diff {:data
|
|
||||||
{:pages-index
|
|
||||||
{(thf/current-page-id file)
|
|
||||||
{:objects
|
|
||||||
{(:id copy2-root-child1)
|
|
||||||
{:component-id [(thi/id :component2) (thi/id :wrong-comp2)]}
|
|
||||||
(:id copy2-root-child2)
|
|
||||||
{:component-id [(thi/id :component1) (thi/id :wrong-comp3)]}}}}}}]
|
|
||||||
|
|
||||||
(thf/dump-file file') ;; Uncomment to debug
|
|
||||||
(t/is (= expected-diff diff)))))
|
|
||||||
|
|
||||||
@ -465,10 +465,9 @@
|
|||||||
page
|
page
|
||||||
{(:id file) file}
|
{(:id file) file}
|
||||||
(thi/id :nested-h-ellipse))
|
(thi/id :nested-h-ellipse))
|
||||||
file' (-> (thf/apply-changes file changes :validate? false)
|
file' (-> (thf/apply-changes file changes)
|
||||||
(tho/propagate-component-changes :c-board-with-ellipse)
|
(tho/propagate-component-changes :c-board-with-ellipse)
|
||||||
(tho/propagate-component-changes :c-big-board)
|
(tho/propagate-component-changes :c-big-board))
|
||||||
(thf/validate-file!))
|
|
||||||
|
|
||||||
;; ==== Get
|
;; ==== Get
|
||||||
nested2-h-ellipse (ths/get-shape file' :nested-h-ellipse)
|
nested2-h-ellipse (ths/get-shape file' :nested-h-ellipse)
|
||||||
|
|||||||
@ -349,73 +349,4 @@
|
|||||||
(t/is (= (:fill-color fill') "#FFFFFF"))
|
(t/is (= (:fill-color fill') "#FFFFFF"))
|
||||||
(t/is (= (:fill-opacity fill') 1))
|
(t/is (= (:fill-opacity fill') 1))
|
||||||
(t/is (= (:touched copy2-root') nil))
|
(t/is (= (:touched copy2-root') nil))
|
||||||
(t/is (= (:touched copy2-child') nil))))
|
(t/is (= (:touched copy2-child') nil))))
|
||||||
|
|
||||||
(t/deftest test-reset-with-propagation-updates-copies
|
|
||||||
;; When a nested copy inside a main component has an override and we
|
|
||||||
;; reset it passing a propagate-fn, the reset must be propagated to
|
|
||||||
;; all copies of that component so they reflect the canonical color.
|
|
||||||
(let [;; ==== Setup
|
|
||||||
file
|
|
||||||
(-> (thf/sample-file :file1)
|
|
||||||
;; component1: main1-root / main1-child (fill "#aabbcc")
|
|
||||||
;; component2: main2-root contains nested-head (instance of component1)
|
|
||||||
;; copy2-root: copy of component2
|
|
||||||
(tho/add-nested-component-with-copy
|
|
||||||
:component1 :main1-root :main1-child
|
|
||||||
:component2 :main2-root :nested-head
|
|
||||||
:copy2-root
|
|
||||||
:main1-child-params {:fills (ths/sample-fills-color :fill-color "#aabbcc")}
|
|
||||||
:copy2-root-params {:children-labels [:copy2-nested-head]}))
|
|
||||||
|
|
||||||
propagate-fn (fn [f]
|
|
||||||
(-> f
|
|
||||||
(tho/propagate-component-changes :component1)
|
|
||||||
(tho/propagate-component-changes :component2)))
|
|
||||||
|
|
||||||
;; ==== Action – override the nested-head color, then reset it with propagation
|
|
||||||
file'
|
|
||||||
(-> file
|
|
||||||
(tho/update-bottom-color :nested-head "#fabada" :propagate-fn propagate-fn)
|
|
||||||
(tho/reset-overrides (ths/get-shape file :nested-head) :propagate-fn propagate-fn))
|
|
||||||
|
|
||||||
;; ==== Get
|
|
||||||
copy2-bottom-color (tho/bottom-fill-color file' :copy2-root)]
|
|
||||||
|
|
||||||
;; ==== Check
|
|
||||||
;; After reset + propagation the copy should mirror the canonical color
|
|
||||||
(t/is (= copy2-bottom-color "#aabbcc"))))
|
|
||||||
|
|
||||||
(t/deftest test-reset-without-propagation-does-not-update-copies
|
|
||||||
;; This is the regression test for the misplaced-parenthesis bug: when
|
|
||||||
;; propagate-fn is NOT passed to reset-overrides the copies of the component
|
|
||||||
;; must still hold the overridden value because the component sync never ran.
|
|
||||||
(let [;; ==== Setup
|
|
||||||
file
|
|
||||||
(-> (thf/sample-file :file1)
|
|
||||||
(tho/add-nested-component-with-copy
|
|
||||||
:component1 :main1-root :main1-child
|
|
||||||
:component2 :main2-root :nested-head
|
|
||||||
:copy2-root
|
|
||||||
:main1-child-params {:fills (ths/sample-fills-color :fill-color "#aabbcc")}
|
|
||||||
:copy2-root-params {:children-labels [:copy2-nested-head]}))
|
|
||||||
|
|
||||||
propagate-fn (fn [f]
|
|
||||||
(-> f
|
|
||||||
(tho/propagate-component-changes :component1)
|
|
||||||
(tho/propagate-component-changes :component2)))
|
|
||||||
|
|
||||||
;; ==== Action – override the nested-head color, then reset WITHOUT propagation
|
|
||||||
file'
|
|
||||||
(-> file
|
|
||||||
(tho/update-bottom-color :nested-head "#fabada" :propagate-fn propagate-fn)
|
|
||||||
;; Reset without propagate-fn: the component definition is updated but
|
|
||||||
;; the change is never pushed to the copy.
|
|
||||||
(tho/reset-overrides (ths/get-shape file :nested-head)))
|
|
||||||
|
|
||||||
;; ==== Get
|
|
||||||
copy2-bottom-color (tho/bottom-fill-color file' :copy2-root)]
|
|
||||||
|
|
||||||
;; ==== Check
|
|
||||||
;; Without propagation the copy still reflects the overridden color
|
|
||||||
(t/is (= copy2-bottom-color "#fabada"))))
|
|
||||||
@ -64,8 +64,9 @@
|
|||||||
|
|
||||||
(reset-all-overrides [file]
|
(reset-all-overrides [file]
|
||||||
(-> file
|
(-> file
|
||||||
(tho/reset-overrides-in-first-child :frame-board-1 :page-label :page-1 :propagate-fn propagate-all-component-changes)
|
(tho/reset-overrides-in-first-child :frame-board-1 :page-label :page-1)
|
||||||
(tho/reset-overrides-in-first-child :copy-board-1 :page-label :page-2 :propagate-fn propagate-all-component-changes)))
|
(tho/reset-overrides-in-first-child :copy-board-1 :page-label :page-2)
|
||||||
|
(propagate-all-component-changes)))
|
||||||
|
|
||||||
(fill-colors [file]
|
(fill-colors [file]
|
||||||
[(tho/bottom-fill-color file :frame-ellipse-1 :page-label :page-1)
|
[(tho/bottom-fill-color file :frame-ellipse-1 :page-label :page-1)
|
||||||
|
|||||||
@ -6,11 +6,20 @@
|
|||||||
|
|
||||||
(ns common-tests.logic.multiple-nesting-levels-test
|
(ns common-tests.logic.multiple-nesting-levels-test
|
||||||
(:require
|
(:require
|
||||||
|
[app.common.files.changes :as ch]
|
||||||
|
[app.common.files.changes-builder :as pcb]
|
||||||
|
[app.common.logic.libraries :as cll]
|
||||||
|
[app.common.logic.shapes :as cls]
|
||||||
|
[app.common.pprint :as pp]
|
||||||
[app.common.test-helpers.components :as thc]
|
[app.common.test-helpers.components :as thc]
|
||||||
[app.common.test-helpers.compositions :as tho]
|
[app.common.test-helpers.compositions :as tho]
|
||||||
[app.common.test-helpers.files :as thf]
|
[app.common.test-helpers.files :as thf]
|
||||||
[app.common.test-helpers.ids-map :as thi]
|
[app.common.test-helpers.ids-map :as thi]
|
||||||
[app.common.test-helpers.shapes :as ths]
|
[app.common.test-helpers.shapes :as ths]
|
||||||
|
[app.common.types.component :as ctk]
|
||||||
|
[app.common.types.container :as ctn]
|
||||||
|
[app.common.types.file :as ctf]
|
||||||
|
[app.common.uuid :as uuid]
|
||||||
[clojure.test :as t]))
|
[clojure.test :as t]))
|
||||||
|
|
||||||
(t/use-fixtures :each thi/test-fixture)
|
(t/use-fixtures :each thi/test-fixture)
|
||||||
@ -47,9 +56,10 @@
|
|||||||
|
|
||||||
(reset-all-overrides [file]
|
(reset-all-overrides [file]
|
||||||
(-> file
|
(-> file
|
||||||
(tho/reset-overrides (ths/get-shape file :copy-simple-1) :propagate-fn propagate-all-component-changes)
|
(tho/reset-overrides (ths/get-shape file :copy-simple-1))
|
||||||
(tho/reset-overrides (ths/get-shape file :copy-frame-composed-1) :propagate-fn propagate-all-component-changes)
|
(tho/reset-overrides (ths/get-shape file :copy-frame-composed-1))
|
||||||
(tho/reset-overrides (ths/get-shape file :composed-1-composed-2-copy) :propagate-fn propagate-all-component-changes)))
|
(tho/reset-overrides (ths/get-shape file :composed-1-composed-2-copy))
|
||||||
|
(propagate-all-component-changes)))
|
||||||
|
|
||||||
(fill-colors [file]
|
(fill-colors [file]
|
||||||
[(tho/bottom-fill-color file :frame-simple-1)
|
[(tho/bottom-fill-color file :frame-simple-1)
|
||||||
|
|||||||
@ -6,12 +6,20 @@
|
|||||||
|
|
||||||
(ns common-tests.logic.swap-as-override-test
|
(ns common-tests.logic.swap-as-override-test
|
||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
[app.common.files.changes :as ch]
|
||||||
|
[app.common.files.changes-builder :as pcb]
|
||||||
|
[app.common.logic.libraries :as cll]
|
||||||
|
[app.common.logic.shapes :as cls]
|
||||||
|
[app.common.pprint :as pp]
|
||||||
[app.common.test-helpers.components :as thc]
|
[app.common.test-helpers.components :as thc]
|
||||||
[app.common.test-helpers.compositions :as tho]
|
[app.common.test-helpers.compositions :as tho]
|
||||||
[app.common.test-helpers.files :as thf]
|
[app.common.test-helpers.files :as thf]
|
||||||
[app.common.test-helpers.ids-map :as thi]
|
[app.common.test-helpers.ids-map :as thi]
|
||||||
[app.common.test-helpers.shapes :as ths]
|
[app.common.test-helpers.shapes :as ths]
|
||||||
|
[app.common.types.component :as ctk]
|
||||||
|
[app.common.types.container :as ctn]
|
||||||
|
[app.common.types.file :as ctf]
|
||||||
|
[app.common.uuid :as uuid]
|
||||||
[clojure.test :as t]))
|
[clojure.test :as t]))
|
||||||
|
|
||||||
(t/use-fixtures :each thi/test-fixture)
|
(t/use-fixtures :each thi/test-fixture)
|
||||||
@ -19,40 +27,23 @@
|
|||||||
(defn- setup []
|
(defn- setup []
|
||||||
(-> (thf/sample-file :file1)
|
(-> (thf/sample-file :file1)
|
||||||
|
|
||||||
(tho/add-simple-component :component-1 :frame-component-1 :child-component-1
|
(tho/add-simple-component :component-1 :frame-component-1 :child-component-1 :child-params {:name "child-component-1" :type :rect :fills (ths/sample-fills-color :fill-color "#111111")})
|
||||||
:root-params {:name "component-1"}
|
(tho/add-simple-component :component-2 :frame-component-2 :child-component-2 :child-params {:name "child-component-2" :type :rect :fills (ths/sample-fills-color :fill-color "#222222")})
|
||||||
:child-params {:name "child-component-1"
|
(tho/add-simple-component :component-3 :frame-component-3 :child-component-3 :child-params {:name "child-component-3" :type :rect :fills (ths/sample-fills-color :fill-color "#333333")})
|
||||||
:type :rect
|
|
||||||
:fills (ths/sample-fills-color :fill-color "#111111")})
|
|
||||||
(tho/add-simple-component :component-2 :frame-component-2 :child-component-2
|
|
||||||
:root-params {:name "component-2"}
|
|
||||||
:child-params {:name "child-component-2"
|
|
||||||
:type :rect
|
|
||||||
:fills (ths/sample-fills-color :fill-color "#222222")})
|
|
||||||
(tho/add-simple-component :component-3 :frame-component-3 :child-component-3
|
|
||||||
:root-params {:name "component-3"}
|
|
||||||
:child-params {:name "child-component-3"
|
|
||||||
:type :rect
|
|
||||||
:fills (ths/sample-fills-color :fill-color "#333333")})
|
|
||||||
|
|
||||||
(tho/add-frame :frame-icon-and-text :name "copy-component-1")
|
(tho/add-frame :frame-icon-and-text)
|
||||||
(thc/instantiate-component :component-1 :copy-component-1
|
(thc/instantiate-component :component-1 :copy-component-1 :parent-label :frame-icon-and-text :children-labels [:component-1-icon-and-text])
|
||||||
:parent-label :frame-icon-and-text
|
|
||||||
:children-labels [:component-1-icon-and-text])
|
|
||||||
(ths/add-sample-shape :text
|
(ths/add-sample-shape :text
|
||||||
{:type :text
|
{:type :text
|
||||||
:name "icon+text"
|
:name "icon+text"
|
||||||
:parent-label :frame-icon-and-text})
|
:parent-label :frame-icon-and-text})
|
||||||
(thc/make-component :icon-and-text :frame-icon-and-text)
|
(thc/make-component :icon-and-text :frame-icon-and-text)
|
||||||
|
|
||||||
(tho/add-frame :frame-panel :name "icon-and-text")
|
(tho/add-frame :frame-panel)
|
||||||
(thc/instantiate-component :icon-and-text :copy-icon-and-text
|
(thc/instantiate-component :icon-and-text :copy-icon-and-text :parent-label :frame-panel :children-labels [:icon-and-text-panel])
|
||||||
:parent-label :frame-panel
|
|
||||||
:children-labels [:icon-and-text-panel])
|
|
||||||
(thc/make-component :panel :frame-panel)
|
(thc/make-component :panel :frame-panel)
|
||||||
|
|
||||||
(thc/instantiate-component :panel :copy-panel
|
(thc/instantiate-component :panel :copy-panel :children-labels [:copy-icon-and-text-panel])))
|
||||||
:children-labels [:copy-icon-and-text-panel])))
|
|
||||||
|
|
||||||
(defn- propagate-all-component-changes [file]
|
(defn- propagate-all-component-changes [file]
|
||||||
(-> file
|
(-> file
|
||||||
|
|||||||
@ -30,7 +30,7 @@
|
|||||||
copy (ths/get-shape file :copy01)
|
copy (ths/get-shape file :copy01)
|
||||||
|
|
||||||
;; ==== Action
|
;; ==== Action
|
||||||
file' (tho/swap-component-in-shape file :copy01 :circle {:new-shape-label :copy02 :keep-touched? true})
|
file' (tho/swap-component file copy :circle {:new-shape-label :copy02 :keep-touched? true})
|
||||||
|
|
||||||
copy' (ths/get-shape file' :copy02)]
|
copy' (ths/get-shape file' :copy02)]
|
||||||
;; Both copies have the same id
|
;; Both copies have the same id
|
||||||
|
|||||||
@ -35,7 +35,7 @@
|
|||||||
copy01 (ths/get-shape file :copy01)
|
copy01 (ths/get-shape file :copy01)
|
||||||
|
|
||||||
;; ==== Action
|
;; ==== Action
|
||||||
file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
||||||
|
|
||||||
copy01' (ths/get-shape file' :copy02)]
|
copy01' (ths/get-shape file' :copy02)]
|
||||||
(thf/dump-file file :keys [:width])
|
(thf/dump-file file :keys [:width])
|
||||||
@ -61,7 +61,7 @@
|
|||||||
rect01 (get-in page [:objects (-> copy01 :shapes first)])
|
rect01 (get-in page [:objects (-> copy01 :shapes first)])
|
||||||
|
|
||||||
;; ==== Action
|
;; ==== Action
|
||||||
file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
||||||
|
|
||||||
page' (thf/current-page file')
|
page' (thf/current-page file')
|
||||||
copy02' (ths/get-shape file' :copy02)
|
copy02' (ths/get-shape file' :copy02)
|
||||||
@ -100,7 +100,7 @@
|
|||||||
copy01 (ths/get-shape file :copy01)
|
copy01 (ths/get-shape file :copy01)
|
||||||
|
|
||||||
;; ==== Action
|
;; ==== Action
|
||||||
file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
||||||
|
|
||||||
copy01' (ths/get-shape file' :copy02)]
|
copy01' (ths/get-shape file' :copy02)]
|
||||||
(thf/dump-file file :keys [:width])
|
(thf/dump-file file :keys [:width])
|
||||||
@ -137,7 +137,7 @@
|
|||||||
rect01 (get-in page [:objects (:id rect01)])
|
rect01 (get-in page [:objects (:id rect01)])
|
||||||
|
|
||||||
;; ==== Action
|
;; ==== Action
|
||||||
file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
||||||
|
|
||||||
page' (thf/current-page file')
|
page' (thf/current-page file')
|
||||||
copy02' (ths/get-shape file' :copy02)
|
copy02' (ths/get-shape file' :copy02)
|
||||||
@ -180,7 +180,7 @@
|
|||||||
rect01 (get-in page [:objects (:id rect01)])
|
rect01 (get-in page [:objects (:id rect01)])
|
||||||
|
|
||||||
;; ==== Action
|
;; ==== Action
|
||||||
file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
||||||
|
|
||||||
page' (thf/current-page file')
|
page' (thf/current-page file')
|
||||||
copy02' (ths/get-shape file' :copy02)
|
copy02' (ths/get-shape file' :copy02)
|
||||||
@ -257,19 +257,25 @@
|
|||||||
|
|
||||||
|
|
||||||
;; The copy clean has no overrides
|
;; The copy clean has no overrides
|
||||||
|
|
||||||
|
|
||||||
|
copy-clean (ths/get-shape file :copy-clean)
|
||||||
copy-clean-t (ths/get-shape file :copy-clean-t)
|
copy-clean-t (ths/get-shape file :copy-clean-t)
|
||||||
|
|
||||||
;; Override font size on copy-font-size
|
;; Override font size on copy-font-size
|
||||||
file (update-attr file :copy-font-size-t font-size-path-0 "25")
|
file (update-attr file :copy-font-size-t font-size-path-0 "25")
|
||||||
|
copy-font-size (ths/get-shape file :copy-font-size)
|
||||||
copy-font-size-t (ths/get-shape file :copy-font-size-t)
|
copy-font-size-t (ths/get-shape file :copy-font-size-t)
|
||||||
|
|
||||||
;; Override text on copy-text
|
;; Override text on copy-text
|
||||||
file (update-attr file :copy-text-t text-path-0 "text overriden")
|
file (update-attr file :copy-text-t text-path-0 "text overriden")
|
||||||
|
copy-text (ths/get-shape file :copy-text)
|
||||||
copy-text-t (ths/get-shape file :copy-text-t)
|
copy-text-t (ths/get-shape file :copy-text-t)
|
||||||
|
|
||||||
;; Override both on copy-both
|
;; Override both on copy-both
|
||||||
file (update-attr file :copy-both-t font-size-path-0 "25")
|
file (update-attr file :copy-both-t font-size-path-0 "25")
|
||||||
file (update-attr file :copy-both-t text-path-0 "text overriden")
|
file (update-attr file :copy-both-t text-path-0 "text overriden")
|
||||||
|
copy-both (ths/get-shape file :copy-both)
|
||||||
copy-both-t (ths/get-shape file :copy-both-t)
|
copy-both-t (ths/get-shape file :copy-both-t)
|
||||||
|
|
||||||
|
|
||||||
@ -277,10 +283,10 @@
|
|||||||
|
|
||||||
|
|
||||||
file' (-> file
|
file' (-> file
|
||||||
(tho/swap-component-in-shape :copy-clean :c02 {:new-shape-label :copy-clean-2 :keep-touched? true})
|
(tho/swap-component copy-clean :c02 {:new-shape-label :copy-clean-2 :keep-touched? true})
|
||||||
(tho/swap-component-in-shape :copy-font-size :c02 {:new-shape-label :copy-font-size-2 :keep-touched? true})
|
(tho/swap-component copy-font-size :c02 {:new-shape-label :copy-font-size-2 :keep-touched? true})
|
||||||
(tho/swap-component-in-shape :copy-text :c02 {:new-shape-label :copy-text-2 :keep-touched? true})
|
(tho/swap-component copy-text :c02 {:new-shape-label :copy-text-2 :keep-touched? true})
|
||||||
(tho/swap-component-in-shape :copy-both :c02 {:new-shape-label :copy-both-2 :keep-touched? true}))
|
(tho/swap-component copy-both :c02 {:new-shape-label :copy-both-2 :keep-touched? true}))
|
||||||
page' (thf/current-page file')
|
page' (thf/current-page file')
|
||||||
copy-clean' (ths/get-shape file' :copy-clean-2)
|
copy-clean' (ths/get-shape file' :copy-clean-2)
|
||||||
copy-clean-t' (get-in page' [:objects (-> copy-clean' :shapes first)])
|
copy-clean-t' (get-in page' [:objects (-> copy-clean' :shapes first)])
|
||||||
@ -381,19 +387,25 @@
|
|||||||
|
|
||||||
|
|
||||||
;; The copy clean has no overrides
|
;; The copy clean has no overrides
|
||||||
|
|
||||||
|
|
||||||
|
copy-clean (ths/get-shape file :copy-clean)
|
||||||
copy-clean-t (ths/get-shape file :copy-clean-t)
|
copy-clean-t (ths/get-shape file :copy-clean-t)
|
||||||
|
|
||||||
;; Override font size on copy-font-size
|
;; Override font size on copy-font-size
|
||||||
file (update-attr file :copy-font-size-t font-size-path-0 "25")
|
file (update-attr file :copy-font-size-t font-size-path-0 "25")
|
||||||
|
copy-font-size (ths/get-shape file :copy-font-size)
|
||||||
copy-font-size-t (ths/get-shape file :copy-font-size-t)
|
copy-font-size-t (ths/get-shape file :copy-font-size-t)
|
||||||
|
|
||||||
;; Override text on copy-text
|
;; Override text on copy-text
|
||||||
file (update-attr file :copy-text-t text-path-0 "text overriden")
|
file (update-attr file :copy-text-t text-path-0 "text overriden")
|
||||||
|
copy-text (ths/get-shape file :copy-text)
|
||||||
copy-text-t (ths/get-shape file :copy-text-t)
|
copy-text-t (ths/get-shape file :copy-text-t)
|
||||||
|
|
||||||
;; Override both on copy-both
|
;; Override both on copy-both
|
||||||
file (update-attr file :copy-both-t font-size-path-0 "25")
|
file (update-attr file :copy-both-t font-size-path-0 "25")
|
||||||
file (update-attr file :copy-both-t text-path-0 "text overriden")
|
file (update-attr file :copy-both-t text-path-0 "text overriden")
|
||||||
|
copy-both (ths/get-shape file :copy-both)
|
||||||
copy-both-t (ths/get-shape file :copy-both-t)
|
copy-both-t (ths/get-shape file :copy-both-t)
|
||||||
|
|
||||||
|
|
||||||
@ -401,10 +413,10 @@
|
|||||||
|
|
||||||
|
|
||||||
file' (-> file
|
file' (-> file
|
||||||
(tho/swap-component-in-shape :copy-clean :c02 {:new-shape-label :copy-clean-2 :keep-touched? true})
|
(tho/swap-component copy-clean :c02 {:new-shape-label :copy-clean-2 :keep-touched? true})
|
||||||
(tho/swap-component-in-shape :copy-font-size :c02 {:new-shape-label :copy-font-size-2 :keep-touched? true})
|
(tho/swap-component copy-font-size :c02 {:new-shape-label :copy-font-size-2 :keep-touched? true})
|
||||||
(tho/swap-component-in-shape :copy-text :c02 {:new-shape-label :copy-text-2 :keep-touched? true})
|
(tho/swap-component copy-text :c02 {:new-shape-label :copy-text-2 :keep-touched? true})
|
||||||
(tho/swap-component-in-shape :copy-both :c02 {:new-shape-label :copy-both-2 :keep-touched? true}))
|
(tho/swap-component copy-both :c02 {:new-shape-label :copy-both-2 :keep-touched? true}))
|
||||||
page' (thf/current-page file')
|
page' (thf/current-page file')
|
||||||
copy-clean' (ths/get-shape file' :copy-clean-2)
|
copy-clean' (ths/get-shape file' :copy-clean-2)
|
||||||
copy-clean-t' (get-in page' [:objects (-> copy-clean' :shapes first)])
|
copy-clean-t' (get-in page' [:objects (-> copy-clean' :shapes first)])
|
||||||
@ -503,19 +515,25 @@
|
|||||||
|
|
||||||
|
|
||||||
;; The copy clean has no overrides
|
;; The copy clean has no overrides
|
||||||
|
|
||||||
|
|
||||||
|
copy-clean (ths/get-shape file :copy-clean)
|
||||||
copy-clean-t (ths/get-shape file :copy-clean-t)
|
copy-clean-t (ths/get-shape file :copy-clean-t)
|
||||||
|
|
||||||
;; Override font size on copy-font-size
|
;; Override font size on copy-font-size
|
||||||
file (update-attr file :copy-font-size-t font-size-path-0 "25")
|
file (update-attr file :copy-font-size-t font-size-path-0 "25")
|
||||||
|
copy-font-size (ths/get-shape file :copy-font-size)
|
||||||
copy-font-size-t (ths/get-shape file :copy-font-size-t)
|
copy-font-size-t (ths/get-shape file :copy-font-size-t)
|
||||||
|
|
||||||
;; Override text on copy-text
|
;; Override text on copy-text
|
||||||
file (update-attr file :copy-text-t text-path-0 "text overriden")
|
file (update-attr file :copy-text-t text-path-0 "text overriden")
|
||||||
|
copy-text (ths/get-shape file :copy-text)
|
||||||
copy-text-t (ths/get-shape file :copy-text-t)
|
copy-text-t (ths/get-shape file :copy-text-t)
|
||||||
|
|
||||||
;; Override both on copy-both
|
;; Override both on copy-both
|
||||||
file (update-attr file :copy-both-t font-size-path-0 "25")
|
file (update-attr file :copy-both-t font-size-path-0 "25")
|
||||||
file (update-attr file :copy-both-t text-path-0 "text overriden")
|
file (update-attr file :copy-both-t text-path-0 "text overriden")
|
||||||
|
copy-both (ths/get-shape file :copy-both)
|
||||||
copy-both-t (ths/get-shape file :copy-both-t)
|
copy-both-t (ths/get-shape file :copy-both-t)
|
||||||
|
|
||||||
|
|
||||||
@ -523,10 +541,10 @@
|
|||||||
|
|
||||||
|
|
||||||
file' (-> file
|
file' (-> file
|
||||||
(tho/swap-component-in-shape :copy-clean :c02 {:new-shape-label :copy-clean-2 :keep-touched? true})
|
(tho/swap-component copy-clean :c02 {:new-shape-label :copy-clean-2 :keep-touched? true})
|
||||||
(tho/swap-component-in-shape :copy-font-size :c02 {:new-shape-label :copy-font-size-2 :keep-touched? true})
|
(tho/swap-component copy-font-size :c02 {:new-shape-label :copy-font-size-2 :keep-touched? true})
|
||||||
(tho/swap-component-in-shape :copy-text :c02 {:new-shape-label :copy-text-2 :keep-touched? true})
|
(tho/swap-component copy-text :c02 {:new-shape-label :copy-text-2 :keep-touched? true})
|
||||||
(tho/swap-component-in-shape :copy-both :c02 {:new-shape-label :copy-both-2 :keep-touched? true}))
|
(tho/swap-component copy-both :c02 {:new-shape-label :copy-both-2 :keep-touched? true}))
|
||||||
page' (thf/current-page file')
|
page' (thf/current-page file')
|
||||||
copy-clean' (ths/get-shape file' :copy-clean-2)
|
copy-clean' (ths/get-shape file' :copy-clean-2)
|
||||||
copy-clean-t' (get-in page' [:objects (-> copy-clean' :shapes first)])
|
copy-clean-t' (get-in page' [:objects (-> copy-clean' :shapes first)])
|
||||||
@ -627,19 +645,25 @@
|
|||||||
|
|
||||||
|
|
||||||
;; The copy clean has no overrides
|
;; The copy clean has no overrides
|
||||||
|
|
||||||
|
|
||||||
|
copy-clean (ths/get-shape file :copy-clean)
|
||||||
copy-clean-t (ths/get-shape file :copy-clean-t)
|
copy-clean-t (ths/get-shape file :copy-clean-t)
|
||||||
|
|
||||||
;; Override font size on copy-font-size
|
;; Override font size on copy-font-size
|
||||||
file (update-attr file :copy-font-size-t font-size-path-0 "25")
|
file (update-attr file :copy-font-size-t font-size-path-0 "25")
|
||||||
|
copy-font-size (ths/get-shape file :copy-font-size)
|
||||||
copy-font-size-t (ths/get-shape file :copy-font-size-t)
|
copy-font-size-t (ths/get-shape file :copy-font-size-t)
|
||||||
|
|
||||||
;; Override text on copy-text
|
;; Override text on copy-text
|
||||||
file (update-attr file :copy-text-t text-path-0 "text overriden")
|
file (update-attr file :copy-text-t text-path-0 "text overriden")
|
||||||
|
copy-text (ths/get-shape file :copy-text)
|
||||||
copy-text-t (ths/get-shape file :copy-text-t)
|
copy-text-t (ths/get-shape file :copy-text-t)
|
||||||
|
|
||||||
;; Override both on copy-both
|
;; Override both on copy-both
|
||||||
file (update-attr file :copy-both-t font-size-path-0 "25")
|
file (update-attr file :copy-both-t font-size-path-0 "25")
|
||||||
file (update-attr file :copy-both-t text-path-0 "text overriden")
|
file (update-attr file :copy-both-t text-path-0 "text overriden")
|
||||||
|
copy-both (ths/get-shape file :copy-both)
|
||||||
copy-both-t (ths/get-shape file :copy-both-t)
|
copy-both-t (ths/get-shape file :copy-both-t)
|
||||||
|
|
||||||
|
|
||||||
@ -647,10 +671,10 @@
|
|||||||
|
|
||||||
|
|
||||||
file' (-> file
|
file' (-> file
|
||||||
(tho/swap-component-in-shape :copy-clean :c02 {:new-shape-label :copy-clean-2 :keep-touched? true})
|
(tho/swap-component copy-clean :c02 {:new-shape-label :copy-clean-2 :keep-touched? true})
|
||||||
(tho/swap-component-in-shape :copy-font-size :c02 {:new-shape-label :copy-font-size-2 :keep-touched? true})
|
(tho/swap-component copy-font-size :c02 {:new-shape-label :copy-font-size-2 :keep-touched? true})
|
||||||
(tho/swap-component-in-shape :copy-text :c02 {:new-shape-label :copy-text-2 :keep-touched? true})
|
(tho/swap-component copy-text :c02 {:new-shape-label :copy-text-2 :keep-touched? true})
|
||||||
(tho/swap-component-in-shape :copy-both :c02 {:new-shape-label :copy-both-2 :keep-touched? true}))
|
(tho/swap-component copy-both :c02 {:new-shape-label :copy-both-2 :keep-touched? true}))
|
||||||
page' (thf/current-page file')
|
page' (thf/current-page file')
|
||||||
copy-clean' (ths/get-shape file' :copy-clean-2)
|
copy-clean' (ths/get-shape file' :copy-clean-2)
|
||||||
copy-clean-t' (get-in page' [:objects (-> copy-clean' :shapes first)])
|
copy-clean-t' (get-in page' [:objects (-> copy-clean' :shapes first)])
|
||||||
@ -750,12 +774,14 @@
|
|||||||
|
|
||||||
|
|
||||||
file (change-structure file :copy-structure-clean-t)
|
file (change-structure file :copy-structure-clean-t)
|
||||||
|
copy-structure-clean (ths/get-shape file :copy-structure-clean)
|
||||||
copy-structure-clean-t (ths/get-shape file :copy-structure-clean-t)
|
copy-structure-clean-t (ths/get-shape file :copy-structure-clean-t)
|
||||||
|
|
||||||
;; Duplicate a text line in copy-structure-clean, updating
|
;; Duplicate a text line in copy-structure-clean, updating
|
||||||
;; both lines with the same attrs
|
;; both lines with the same attrs
|
||||||
file (-> (update-attr file :copy-structure-unif-t font-size-path-0 "25")
|
file (-> (update-attr file :copy-structure-unif-t font-size-path-0 "25")
|
||||||
(change-structure :copy-structure-unif-t))
|
(change-structure :copy-structure-unif-t))
|
||||||
|
copy-structure-unif (ths/get-shape file :copy-structure-unif)
|
||||||
copy-structure-unif-t (ths/get-shape file :copy-structure-unif-t)
|
copy-structure-unif-t (ths/get-shape file :copy-structure-unif-t)
|
||||||
|
|
||||||
;; Duplicate a text line in copy-structure-clean, updating
|
;; Duplicate a text line in copy-structure-clean, updating
|
||||||
@ -763,6 +789,7 @@
|
|||||||
file (-> (change-structure file :copy-structure-mixed-t)
|
file (-> (change-structure file :copy-structure-mixed-t)
|
||||||
(update-attr :copy-structure-mixed-t font-size-path-0 "35")
|
(update-attr :copy-structure-mixed-t font-size-path-0 "35")
|
||||||
(update-attr :copy-structure-mixed-t font-size-path-1 "40"))
|
(update-attr :copy-structure-mixed-t font-size-path-1 "40"))
|
||||||
|
copy-structure-mixed (ths/get-shape file :copy-structure-mixed)
|
||||||
copy-structure-mixed-t (ths/get-shape file :copy-structure-mixed-t)
|
copy-structure-mixed-t (ths/get-shape file :copy-structure-mixed-t)
|
||||||
|
|
||||||
|
|
||||||
@ -770,9 +797,9 @@
|
|||||||
|
|
||||||
|
|
||||||
file' (-> file
|
file' (-> file
|
||||||
(tho/swap-component-in-shape :copy-structure-clean :c02 {:new-shape-label :copy-structure-clean-2 :keep-touched? true})
|
(tho/swap-component copy-structure-clean :c02 {:new-shape-label :copy-structure-clean-2 :keep-touched? true})
|
||||||
(tho/swap-component-in-shape :copy-structure-unif :c02 {:new-shape-label :copy-structure-unif-2 :keep-touched? true})
|
(tho/swap-component copy-structure-unif :c02 {:new-shape-label :copy-structure-unif-2 :keep-touched? true})
|
||||||
(tho/swap-component-in-shape :copy-structure-mixed :c02 {:new-shape-label :copy-structure-mixed-2 :keep-touched? true}))
|
(tho/swap-component copy-structure-mixed :c02 {:new-shape-label :copy-structure-mixed-2 :keep-touched? true}))
|
||||||
page' (thf/current-page file')
|
page' (thf/current-page file')
|
||||||
copy-structure-clean' (ths/get-shape file' :copy-structure-clean-2)
|
copy-structure-clean' (ths/get-shape file' :copy-structure-clean-2)
|
||||||
copy-structure-clean-t' (get-in page' [:objects (-> copy-structure-clean' :shapes first)])
|
copy-structure-clean-t' (get-in page' [:objects (-> copy-structure-clean' :shapes first)])
|
||||||
@ -881,12 +908,14 @@
|
|||||||
|
|
||||||
|
|
||||||
file (change-structure file :copy-structure-clean-t)
|
file (change-structure file :copy-structure-clean-t)
|
||||||
|
copy-structure-clean (ths/get-shape file :copy-structure-clean)
|
||||||
copy-structure-clean-t (ths/get-shape file :copy-structure-clean-t)
|
copy-structure-clean-t (ths/get-shape file :copy-structure-clean-t)
|
||||||
|
|
||||||
;; Duplicate a text line in copy-structure-clean, updating
|
;; Duplicate a text line in copy-structure-clean, updating
|
||||||
;; both lines with the same attrs
|
;; both lines with the same attrs
|
||||||
file (-> (update-attr file :copy-structure-unif-t font-size-path-0 "25")
|
file (-> (update-attr file :copy-structure-unif-t font-size-path-0 "25")
|
||||||
(change-structure :copy-structure-unif-t))
|
(change-structure :copy-structure-unif-t))
|
||||||
|
copy-structure-unif (ths/get-shape file :copy-structure-unif)
|
||||||
copy-structure-unif-t (ths/get-shape file :copy-structure-unif-t)
|
copy-structure-unif-t (ths/get-shape file :copy-structure-unif-t)
|
||||||
|
|
||||||
;; Duplicate a text line in copy-structure-clean, updating
|
;; Duplicate a text line in copy-structure-clean, updating
|
||||||
@ -894,6 +923,7 @@
|
|||||||
file (-> (change-structure file :copy-structure-mixed-t)
|
file (-> (change-structure file :copy-structure-mixed-t)
|
||||||
(update-attr :copy-structure-mixed-t font-size-path-0 "35")
|
(update-attr :copy-structure-mixed-t font-size-path-0 "35")
|
||||||
(update-attr :copy-structure-mixed-t font-size-path-1 "40"))
|
(update-attr :copy-structure-mixed-t font-size-path-1 "40"))
|
||||||
|
copy-structure-mixed (ths/get-shape file :copy-structure-mixed)
|
||||||
copy-structure-mixed-t (ths/get-shape file :copy-structure-mixed-t)
|
copy-structure-mixed-t (ths/get-shape file :copy-structure-mixed-t)
|
||||||
|
|
||||||
|
|
||||||
@ -901,9 +931,9 @@
|
|||||||
|
|
||||||
|
|
||||||
file' (-> file
|
file' (-> file
|
||||||
(tho/swap-component-in-shape :copy-structure-clean :c02 {:new-shape-label :copy-structure-clean-2 :keep-touched? true})
|
(tho/swap-component copy-structure-clean :c02 {:new-shape-label :copy-structure-clean-2 :keep-touched? true})
|
||||||
(tho/swap-component-in-shape :copy-structure-unif :c02 {:new-shape-label :copy-structure-unif-2 :keep-touched? true})
|
(tho/swap-component copy-structure-unif :c02 {:new-shape-label :copy-structure-unif-2 :keep-touched? true})
|
||||||
(tho/swap-component-in-shape :copy-structure-mixed :c02 {:new-shape-label :copy-structure-mixed-2 :keep-touched? true}))
|
(tho/swap-component copy-structure-mixed :c02 {:new-shape-label :copy-structure-mixed-2 :keep-touched? true}))
|
||||||
page' (thf/current-page file')
|
page' (thf/current-page file')
|
||||||
copy-structure-clean' (ths/get-shape file' :copy-structure-clean-2)
|
copy-structure-clean' (ths/get-shape file' :copy-structure-clean-2)
|
||||||
copy-structure-clean-t' (get-in page' [:objects (-> copy-structure-clean' :shapes first)])
|
copy-structure-clean-t' (get-in page' [:objects (-> copy-structure-clean' :shapes first)])
|
||||||
@ -1008,12 +1038,14 @@
|
|||||||
|
|
||||||
|
|
||||||
file (change-structure file :copy-structure-clean-t)
|
file (change-structure file :copy-structure-clean-t)
|
||||||
|
copy-structure-clean (ths/get-shape file :copy-structure-clean)
|
||||||
copy-structure-clean-t (ths/get-shape file :copy-structure-clean-t)
|
copy-structure-clean-t (ths/get-shape file :copy-structure-clean-t)
|
||||||
|
|
||||||
;; Duplicate a text line in copy-structure-clean, updating
|
;; Duplicate a text line in copy-structure-clean, updating
|
||||||
;; both lines with the same attrs
|
;; both lines with the same attrs
|
||||||
file (-> (update-attr file :copy-structure-unif-t font-size-path-0 "25")
|
file (-> (update-attr file :copy-structure-unif-t font-size-path-0 "25")
|
||||||
(change-structure :copy-structure-unif-t))
|
(change-structure :copy-structure-unif-t))
|
||||||
|
copy-structure-unif (ths/get-shape file :copy-structure-unif)
|
||||||
copy-structure-unif-t (ths/get-shape file :copy-structure-unif-t)
|
copy-structure-unif-t (ths/get-shape file :copy-structure-unif-t)
|
||||||
|
|
||||||
;; Duplicate a text line in copy-structure-clean, updating
|
;; Duplicate a text line in copy-structure-clean, updating
|
||||||
@ -1021,6 +1053,7 @@
|
|||||||
file (-> (change-structure file :copy-structure-mixed-t)
|
file (-> (change-structure file :copy-structure-mixed-t)
|
||||||
(update-attr :copy-structure-mixed-t font-size-path-0 "35")
|
(update-attr :copy-structure-mixed-t font-size-path-0 "35")
|
||||||
(update-attr :copy-structure-mixed-t font-size-path-1 "40"))
|
(update-attr :copy-structure-mixed-t font-size-path-1 "40"))
|
||||||
|
copy-structure-mixed (ths/get-shape file :copy-structure-mixed)
|
||||||
copy-structure-mixed-t (ths/get-shape file :copy-structure-mixed-t)
|
copy-structure-mixed-t (ths/get-shape file :copy-structure-mixed-t)
|
||||||
|
|
||||||
|
|
||||||
@ -1028,9 +1061,9 @@
|
|||||||
|
|
||||||
|
|
||||||
file' (-> file
|
file' (-> file
|
||||||
(tho/swap-component-in-shape :copy-structure-clean :c02 {:new-shape-label :copy-structure-clean-2 :keep-touched? true})
|
(tho/swap-component copy-structure-clean :c02 {:new-shape-label :copy-structure-clean-2 :keep-touched? true})
|
||||||
(tho/swap-component-in-shape :copy-structure-unif :c02 {:new-shape-label :copy-structure-unif-2 :keep-touched? true})
|
(tho/swap-component copy-structure-unif :c02 {:new-shape-label :copy-structure-unif-2 :keep-touched? true})
|
||||||
(tho/swap-component-in-shape :copy-structure-mixed :c02 {:new-shape-label :copy-structure-mixed-2 :keep-touched? true}))
|
(tho/swap-component copy-structure-mixed :c02 {:new-shape-label :copy-structure-mixed-2 :keep-touched? true}))
|
||||||
page' (thf/current-page file')
|
page' (thf/current-page file')
|
||||||
copy-structure-clean' (ths/get-shape file' :copy-structure-clean-2)
|
copy-structure-clean' (ths/get-shape file' :copy-structure-clean-2)
|
||||||
copy-structure-clean-t' (get-in page' [:objects (-> copy-structure-clean' :shapes first)])
|
copy-structure-clean-t' (get-in page' [:objects (-> copy-structure-clean' :shapes first)])
|
||||||
@ -1136,12 +1169,14 @@
|
|||||||
|
|
||||||
|
|
||||||
file (change-structure file :copy-structure-clean-t)
|
file (change-structure file :copy-structure-clean-t)
|
||||||
|
copy-structure-clean (ths/get-shape file :copy-structure-clean)
|
||||||
copy-structure-clean-t (ths/get-shape file :copy-structure-clean-t)
|
copy-structure-clean-t (ths/get-shape file :copy-structure-clean-t)
|
||||||
|
|
||||||
;; Duplicate a text line in copy-structure-clean, updating
|
;; Duplicate a text line in copy-structure-clean, updating
|
||||||
;; both lines with the same attrs
|
;; both lines with the same attrs
|
||||||
file (-> (update-attr file :copy-structure-unif-t font-size-path-0 "25")
|
file (-> (update-attr file :copy-structure-unif-t font-size-path-0 "25")
|
||||||
(change-structure :copy-structure-unif-t))
|
(change-structure :copy-structure-unif-t))
|
||||||
|
copy-structure-unif (ths/get-shape file :copy-structure-unif)
|
||||||
copy-structure-unif-t (ths/get-shape file :copy-structure-unif-t)
|
copy-structure-unif-t (ths/get-shape file :copy-structure-unif-t)
|
||||||
|
|
||||||
;; Duplicate a text line in copy-structure-clean, updating
|
;; Duplicate a text line in copy-structure-clean, updating
|
||||||
@ -1149,6 +1184,7 @@
|
|||||||
file (-> (change-structure file :copy-structure-mixed-t)
|
file (-> (change-structure file :copy-structure-mixed-t)
|
||||||
(update-attr :copy-structure-mixed-t font-size-path-0 "35")
|
(update-attr :copy-structure-mixed-t font-size-path-0 "35")
|
||||||
(update-attr :copy-structure-mixed-t font-size-path-1 "40"))
|
(update-attr :copy-structure-mixed-t font-size-path-1 "40"))
|
||||||
|
copy-structure-mixed (ths/get-shape file :copy-structure-mixed)
|
||||||
copy-structure-mixed-t (ths/get-shape file :copy-structure-mixed-t)
|
copy-structure-mixed-t (ths/get-shape file :copy-structure-mixed-t)
|
||||||
|
|
||||||
|
|
||||||
@ -1156,9 +1192,9 @@
|
|||||||
|
|
||||||
|
|
||||||
file' (-> file
|
file' (-> file
|
||||||
(tho/swap-component-in-shape :copy-structure-clean :c02 {:new-shape-label :copy-structure-clean-2 :keep-touched? true})
|
(tho/swap-component copy-structure-clean :c02 {:new-shape-label :copy-structure-clean-2 :keep-touched? true})
|
||||||
(tho/swap-component-in-shape :copy-structure-unif :c02 {:new-shape-label :copy-structure-unif-2 :keep-touched? true})
|
(tho/swap-component copy-structure-unif :c02 {:new-shape-label :copy-structure-unif-2 :keep-touched? true})
|
||||||
(tho/swap-component-in-shape :copy-structure-mixed :c02 {:new-shape-label :copy-structure-mixed-2 :keep-touched? true}))
|
(tho/swap-component copy-structure-mixed :c02 {:new-shape-label :copy-structure-mixed-2 :keep-touched? true}))
|
||||||
page' (thf/current-page file')
|
page' (thf/current-page file')
|
||||||
copy-structure-clean' (ths/get-shape file' :copy-structure-clean-2)
|
copy-structure-clean' (ths/get-shape file' :copy-structure-clean-2)
|
||||||
copy-structure-clean-t' (get-in page' [:objects (-> copy-structure-clean' :shapes first)])
|
copy-structure-clean-t' (get-in page' [:objects (-> copy-structure-clean' :shapes first)])
|
||||||
@ -1254,6 +1290,7 @@
|
|||||||
:children-labels [:copy-cp01]))
|
:children-labels [:copy-cp01]))
|
||||||
|
|
||||||
page (thf/current-page file)
|
page (thf/current-page file)
|
||||||
|
copy01 (ths/get-shape file :copy01)
|
||||||
copy-cp01 (ths/get-shape file :copy-cp01)
|
copy-cp01 (ths/get-shape file :copy-cp01)
|
||||||
copy-cp01-rect-id (-> copy-cp01 :shapes first)
|
copy-cp01-rect-id (-> copy-cp01 :shapes first)
|
||||||
|
|
||||||
@ -1272,7 +1309,7 @@
|
|||||||
|
|
||||||
;; ==== Action
|
;; ==== Action
|
||||||
;; Switch :c01 for :c02
|
;; Switch :c01 for :c02
|
||||||
file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
||||||
copy02 (ths/get-shape file' :copy02)
|
copy02 (ths/get-shape file' :copy02)
|
||||||
copy-cp02' (ths/get-shape-by-id file' (-> copy02 :shapes first))
|
copy-cp02' (ths/get-shape-by-id file' (-> copy02 :shapes first))
|
||||||
copy-cp02-rect' (ths/get-shape-by-id file' (-> copy-cp02' :shapes first))]
|
copy-cp02-rect' (ths/get-shape-by-id file' (-> copy-cp02' :shapes first))]
|
||||||
@ -1300,16 +1337,17 @@
|
|||||||
:children-labels [:copy-cp01]))
|
:children-labels [:copy-cp01]))
|
||||||
|
|
||||||
copy01 (ths/get-shape file :copy01)
|
copy01 (ths/get-shape file :copy01)
|
||||||
|
copy-cp01 (ths/get-shape file :copy-cp01)
|
||||||
external02 (thc/get-component file :external02)
|
external02 (thc/get-component file :external02)
|
||||||
|
|
||||||
;; On :c01, swap the copy of :external01 for a copy of :external02
|
;; On :c01, swap the copy of :external01 for a copy of :external02
|
||||||
file (-> file
|
file (-> file
|
||||||
(tho/swap-component-in-shape :copy-cp01 :external02 {:new-shape-label :copy-cp02 :keep-touched? false}))
|
(tho/swap-component copy-cp01 :external02 {:new-shape-label :copy-cp02 :keep-touched? false}))
|
||||||
copy-cp02 (ths/get-shape file :copy-cp02)
|
copy-cp02 (ths/get-shape file :copy-cp02)
|
||||||
|
|
||||||
;; ==== Action
|
;; ==== Action
|
||||||
;; Switch :c01 for :c02
|
;; Switch :c01 for :c02
|
||||||
file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
||||||
|
|
||||||
copy02' (ths/get-shape file' :copy02)
|
copy02' (ths/get-shape file' :copy02)
|
||||||
copy-cp02' (ths/get-shape file' :copy-cp02)]
|
copy-cp02' (ths/get-shape file' :copy-cp02)]
|
||||||
@ -1338,11 +1376,12 @@
|
|||||||
|
|
||||||
page (thf/current-page file)
|
page (thf/current-page file)
|
||||||
copy01 (ths/get-shape file :copy01)
|
copy01 (ths/get-shape file :copy01)
|
||||||
|
copy-cp01 (ths/get-shape file :copy-cp01)
|
||||||
external02 (thc/get-component file :external02)
|
external02 (thc/get-component file :external02)
|
||||||
|
|
||||||
;; On :c01, swap the copy of :external01 for a copy of :external02
|
;; On :c01, swap the copy of :external01 for a copy of :external02
|
||||||
file (-> file
|
file (-> file
|
||||||
(tho/swap-component-in-shape :copy-cp01 :external02 {:new-shape-label :copy-cp02 :keep-touched? false}))
|
(tho/swap-component copy-cp01 :external02 {:new-shape-label :copy-cp02 :keep-touched? false}))
|
||||||
copy-cp02 (ths/get-shape file :copy-cp02)
|
copy-cp02 (ths/get-shape file :copy-cp02)
|
||||||
copy-cp02-rect-id (-> copy-cp02 :shapes first)
|
copy-cp02-rect-id (-> copy-cp02 :shapes first)
|
||||||
|
|
||||||
@ -1357,7 +1396,7 @@
|
|||||||
|
|
||||||
;; ==== Action
|
;; ==== Action
|
||||||
;; Switch :c01 for :c02
|
;; Switch :c01 for :c02
|
||||||
file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
||||||
|
|
||||||
copy02' (ths/get-shape file' :copy02)
|
copy02' (ths/get-shape file' :copy02)
|
||||||
copy-cp02' (ths/get-shape file' :copy-cp02)
|
copy-cp02' (ths/get-shape file' :copy-cp02)
|
||||||
@ -1424,7 +1463,7 @@
|
|||||||
;; ==== Action
|
;; ==== Action
|
||||||
|
|
||||||
|
|
||||||
file' (tho/swap-component-in-shape file :c01-in-copy :c02 {:new-shape-label :c02-in-copy :keep-touched? true})
|
file' (tho/swap-component file c01-in-copy :c02 {:new-shape-label :c02-in-copy :keep-touched? true})
|
||||||
|
|
||||||
page' (thf/current-page file')
|
page' (thf/current-page file')
|
||||||
c02-in-copy' (ths/get-shape file' :c02-in-copy)
|
c02-in-copy' (ths/get-shape file' :c02-in-copy)
|
||||||
@ -1476,7 +1515,7 @@
|
|||||||
rect01 (get-in page [:objects (:id rect01)])
|
rect01 (get-in page [:objects (:id rect01)])
|
||||||
|
|
||||||
;; ==== Action
|
;; ==== Action
|
||||||
file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
||||||
|
|
||||||
page' (thf/current-page file')
|
page' (thf/current-page file')
|
||||||
copy02' (ths/get-shape file' :copy02)
|
copy02' (ths/get-shape file' :copy02)
|
||||||
@ -1525,7 +1564,7 @@
|
|||||||
rect01 (get-in page [:objects (:id rect01)])
|
rect01 (get-in page [:objects (:id rect01)])
|
||||||
|
|
||||||
;; ==== Action
|
;; ==== Action
|
||||||
file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
||||||
|
|
||||||
page' (thf/current-page file')
|
page' (thf/current-page file')
|
||||||
copy02' (ths/get-shape file' :copy02)
|
copy02' (ths/get-shape file' :copy02)
|
||||||
@ -1574,7 +1613,7 @@
|
|||||||
rect01 (get-in page [:objects (:id rect01)])
|
rect01 (get-in page [:objects (:id rect01)])
|
||||||
|
|
||||||
;; ==== Action
|
;; ==== Action
|
||||||
file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
||||||
|
|
||||||
page' (thf/current-page file')
|
page' (thf/current-page file')
|
||||||
copy02' (ths/get-shape file' :copy02)
|
copy02' (ths/get-shape file' :copy02)
|
||||||
@ -1621,7 +1660,7 @@
|
|||||||
rect01 (get-in page [:objects (:id rect01)])
|
rect01 (get-in page [:objects (:id rect01)])
|
||||||
|
|
||||||
;; ==== Action
|
;; ==== Action
|
||||||
file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
||||||
|
|
||||||
page' (thf/current-page file')
|
page' (thf/current-page file')
|
||||||
copy02' (ths/get-shape file' :copy02)
|
copy02' (ths/get-shape file' :copy02)
|
||||||
@ -1675,7 +1714,7 @@
|
|||||||
rect01 (get-in page [:objects (:id rect01)])
|
rect01 (get-in page [:objects (:id rect01)])
|
||||||
|
|
||||||
;; ==== Action
|
;; ==== Action
|
||||||
file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
||||||
|
|
||||||
page' (thf/current-page file')
|
page' (thf/current-page file')
|
||||||
copy02' (ths/get-shape file' :copy02)
|
copy02' (ths/get-shape file' :copy02)
|
||||||
@ -1724,7 +1763,7 @@
|
|||||||
rect01 (get-in page [:objects (:id rect01)])
|
rect01 (get-in page [:objects (:id rect01)])
|
||||||
|
|
||||||
;; ==== Action
|
;; ==== Action
|
||||||
file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
||||||
|
|
||||||
page' (thf/current-page file')
|
page' (thf/current-page file')
|
||||||
copy02' (ths/get-shape file' :copy02)
|
copy02' (ths/get-shape file' :copy02)
|
||||||
@ -1773,7 +1812,7 @@
|
|||||||
rect01 (get-in page [:objects (:id rect01)])
|
rect01 (get-in page [:objects (:id rect01)])
|
||||||
|
|
||||||
;; ==== Action
|
;; ==== Action
|
||||||
file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
||||||
|
|
||||||
page' (thf/current-page file')
|
page' (thf/current-page file')
|
||||||
copy02' (ths/get-shape file' :copy02)
|
copy02' (ths/get-shape file' :copy02)
|
||||||
@ -1820,7 +1859,7 @@
|
|||||||
rect01 (get-in page [:objects (:id rect01)])
|
rect01 (get-in page [:objects (:id rect01)])
|
||||||
|
|
||||||
;; ==== Action
|
;; ==== Action
|
||||||
file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
||||||
|
|
||||||
page' (thf/current-page file')
|
page' (thf/current-page file')
|
||||||
copy02' (ths/get-shape file' :copy02)
|
copy02' (ths/get-shape file' :copy02)
|
||||||
@ -1871,7 +1910,7 @@
|
|||||||
rect01 (get-in page [:objects (:id rect01)])
|
rect01 (get-in page [:objects (:id rect01)])
|
||||||
|
|
||||||
;; ==== Action
|
;; ==== Action
|
||||||
file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
||||||
|
|
||||||
page' (thf/current-page file')
|
page' (thf/current-page file')
|
||||||
copy02' (ths/get-shape file' :copy02)
|
copy02' (ths/get-shape file' :copy02)
|
||||||
@ -1917,7 +1956,7 @@
|
|||||||
rect01 (get-in page [:objects (:id rect01)])
|
rect01 (get-in page [:objects (:id rect01)])
|
||||||
|
|
||||||
;; ==== Action
|
;; ==== Action
|
||||||
file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
||||||
|
|
||||||
page' (thf/current-page file')
|
page' (thf/current-page file')
|
||||||
copy02' (ths/get-shape file' :copy02)
|
copy02' (ths/get-shape file' :copy02)
|
||||||
@ -1984,7 +2023,7 @@
|
|||||||
text01 (get-in page [:objects (:id text01)])
|
text01 (get-in page [:objects (:id text01)])
|
||||||
|
|
||||||
;; ==== Action
|
;; ==== Action
|
||||||
file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
||||||
|
|
||||||
page' (thf/current-page file')
|
page' (thf/current-page file')
|
||||||
copy02' (ths/get-shape file' :copy02)
|
copy02' (ths/get-shape file' :copy02)
|
||||||
@ -2016,7 +2055,7 @@
|
|||||||
rect01 (get-in page [:objects (-> copy01 :shapes first)])
|
rect01 (get-in page [:objects (-> copy01 :shapes first)])
|
||||||
|
|
||||||
;; ==== Action - Try to switch to a component with different shape type
|
;; ==== Action - Try to switch to a component with different shape type
|
||||||
file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
||||||
|
|
||||||
page' (thf/current-page file')
|
page' (thf/current-page file')
|
||||||
copy02' (ths/get-shape file' :copy02)
|
copy02' (ths/get-shape file' :copy02)
|
||||||
@ -2059,7 +2098,7 @@
|
|||||||
path01 (get-in page [:objects (:id path01)])
|
path01 (get-in page [:objects (:id path01)])
|
||||||
|
|
||||||
;; ==== Action
|
;; ==== Action
|
||||||
file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
||||||
|
|
||||||
page' (thf/current-page file')
|
page' (thf/current-page file')
|
||||||
copy02' (ths/get-shape file' :copy02)
|
copy02' (ths/get-shape file' :copy02)
|
||||||
@ -2107,7 +2146,7 @@
|
|||||||
rect01 (get-in page [:objects (:id rect01)])
|
rect01 (get-in page [:objects (:id rect01)])
|
||||||
|
|
||||||
;; ==== Action
|
;; ==== Action
|
||||||
file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
||||||
|
|
||||||
page' (thf/current-page file')
|
page' (thf/current-page file')
|
||||||
copy02' (ths/get-shape file' :copy02)
|
copy02' (ths/get-shape file' :copy02)
|
||||||
@ -2151,7 +2190,7 @@
|
|||||||
rect01 (get-in page [:objects (-> copy01 :shapes first)])
|
rect01 (get-in page [:objects (-> copy01 :shapes first)])
|
||||||
|
|
||||||
;; ==== Action
|
;; ==== Action
|
||||||
file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
||||||
|
|
||||||
page' (thf/current-page file')
|
page' (thf/current-page file')
|
||||||
copy02' (ths/get-shape file' :copy02)
|
copy02' (ths/get-shape file' :copy02)
|
||||||
@ -2204,7 +2243,7 @@
|
|||||||
old-position-data (:position-data text01)
|
old-position-data (:position-data text01)
|
||||||
|
|
||||||
;; ==== Action
|
;; ==== Action
|
||||||
file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
||||||
|
|
||||||
page' (thf/current-page file')
|
page' (thf/current-page file')
|
||||||
copy02' (ths/get-shape file' :copy02)
|
copy02' (ths/get-shape file' :copy02)
|
||||||
@ -2267,7 +2306,7 @@
|
|||||||
rect01 (get-in page [:objects (:id rect01)])
|
rect01 (get-in page [:objects (:id rect01)])
|
||||||
|
|
||||||
;; ==== Action
|
;; ==== Action
|
||||||
file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
||||||
|
|
||||||
page' (thf/current-page file')
|
page' (thf/current-page file')
|
||||||
copy02' (ths/get-shape file' :copy02)
|
copy02' (ths/get-shape file' :copy02)
|
||||||
@ -2318,7 +2357,7 @@
|
|||||||
rect01 (get-in page [:objects (:id rect01)])
|
rect01 (get-in page [:objects (:id rect01)])
|
||||||
|
|
||||||
;; ==== Action
|
;; ==== Action
|
||||||
file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
||||||
|
|
||||||
page' (thf/current-page file')
|
page' (thf/current-page file')
|
||||||
copy02' (ths/get-shape file' :copy02)
|
copy02' (ths/get-shape file' :copy02)
|
||||||
@ -2372,7 +2411,7 @@
|
|||||||
rect01 (get-in page [:objects (:id rect01)])
|
rect01 (get-in page [:objects (:id rect01)])
|
||||||
|
|
||||||
;; ==== Action
|
;; ==== Action
|
||||||
file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
||||||
|
|
||||||
page' (thf/current-page file')
|
page' (thf/current-page file')
|
||||||
copy02' (ths/get-shape file' :copy02)
|
copy02' (ths/get-shape file' :copy02)
|
||||||
@ -2429,7 +2468,7 @@
|
|||||||
rect01 (get-in page [:objects (:id rect01)])
|
rect01 (get-in page [:objects (:id rect01)])
|
||||||
|
|
||||||
;; ==== Action
|
;; ==== Action
|
||||||
file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
||||||
|
|
||||||
page' (thf/current-page file')
|
page' (thf/current-page file')
|
||||||
copy02' (ths/get-shape file' :copy02)
|
copy02' (ths/get-shape file' :copy02)
|
||||||
@ -2493,7 +2532,7 @@
|
|||||||
rect01 (get-in page [:objects (:id rect01)])
|
rect01 (get-in page [:objects (:id rect01)])
|
||||||
|
|
||||||
;; ==== Action
|
;; ==== Action
|
||||||
file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
||||||
|
|
||||||
page' (thf/current-page file')
|
page' (thf/current-page file')
|
||||||
copy02' (ths/get-shape file' :copy02)
|
copy02' (ths/get-shape file' :copy02)
|
||||||
@ -2549,7 +2588,7 @@
|
|||||||
rect01 (get-in page [:objects (:id rect01)])
|
rect01 (get-in page [:objects (:id rect01)])
|
||||||
|
|
||||||
;; ==== Action
|
;; ==== Action
|
||||||
file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
||||||
|
|
||||||
page' (thf/current-page file')
|
page' (thf/current-page file')
|
||||||
copy02' (ths/get-shape file' :copy02)
|
copy02' (ths/get-shape file' :copy02)
|
||||||
@ -2614,7 +2653,7 @@
|
|||||||
rect01 (get-in page [:objects (:id rect01)])
|
rect01 (get-in page [:objects (:id rect01)])
|
||||||
|
|
||||||
;; ==== Action
|
;; ==== Action
|
||||||
file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
||||||
|
|
||||||
page' (thf/current-page file')
|
page' (thf/current-page file')
|
||||||
copy02' (ths/get-shape file' :copy02)
|
copy02' (ths/get-shape file' :copy02)
|
||||||
@ -2671,7 +2710,7 @@
|
|||||||
rect01 (get-in page [:objects (:id rect01)])
|
rect01 (get-in page [:objects (:id rect01)])
|
||||||
|
|
||||||
;; ==== Action
|
;; ==== Action
|
||||||
file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
||||||
|
|
||||||
page' (thf/current-page file')
|
page' (thf/current-page file')
|
||||||
copy02' (ths/get-shape file' :copy02)
|
copy02' (ths/get-shape file' :copy02)
|
||||||
|
|||||||
@ -8,7 +8,6 @@
|
|||||||
(:require
|
(:require
|
||||||
#?(:clj [common-tests.fressian-test])
|
#?(:clj [common-tests.fressian-test])
|
||||||
[clojure.test :as t]
|
[clojure.test :as t]
|
||||||
[common-tests.attrs-test]
|
|
||||||
[common-tests.buffer-test]
|
[common-tests.buffer-test]
|
||||||
[common-tests.colors-test]
|
[common-tests.colors-test]
|
||||||
[common-tests.data-test]
|
[common-tests.data-test]
|
||||||
@ -55,7 +54,6 @@
|
|||||||
[common-tests.path-names-test]
|
[common-tests.path-names-test]
|
||||||
[common-tests.record-test]
|
[common-tests.record-test]
|
||||||
[common-tests.schema-test]
|
[common-tests.schema-test]
|
||||||
[common-tests.spec-test]
|
|
||||||
[common-tests.svg-path-test]
|
[common-tests.svg-path-test]
|
||||||
[common-tests.svg-test]
|
[common-tests.svg-test]
|
||||||
[common-tests.text-test]
|
[common-tests.text-test]
|
||||||
@ -87,7 +85,6 @@
|
|||||||
(defn -main
|
(defn -main
|
||||||
[& args]
|
[& args]
|
||||||
(t/run-tests
|
(t/run-tests
|
||||||
'common-tests.attrs-test
|
|
||||||
'common-tests.buffer-test
|
'common-tests.buffer-test
|
||||||
'common-tests.colors-test
|
'common-tests.colors-test
|
||||||
'common-tests.data-test
|
'common-tests.data-test
|
||||||
@ -135,7 +132,6 @@
|
|||||||
'common-tests.path-names-test
|
'common-tests.path-names-test
|
||||||
'common-tests.record-test
|
'common-tests.record-test
|
||||||
'common-tests.schema-test
|
'common-tests.schema-test
|
||||||
'common-tests.spec-test
|
|
||||||
'common-tests.svg-path-test
|
'common-tests.svg-path-test
|
||||||
'common-tests.svg-test
|
'common-tests.svg-test
|
||||||
'common-tests.text-test
|
'common-tests.text-test
|
||||||
|
|||||||
@ -1,89 +0,0 @@
|
|||||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
||||||
;;
|
|
||||||
;; Copyright (c) KALEIDOS INC
|
|
||||||
|
|
||||||
(ns common-tests.spec-test
|
|
||||||
(:require
|
|
||||||
[app.common.spec :as spec]
|
|
||||||
[clojure.test :as t]))
|
|
||||||
|
|
||||||
(t/deftest valid-emails
|
|
||||||
(t/testing "accepts well-formed email addresses"
|
|
||||||
(doseq [email ["user@domain.com"
|
|
||||||
"user.name@domain.com"
|
|
||||||
"user+tag@domain.com"
|
|
||||||
"user-name@domain.com"
|
|
||||||
"user_name@domain.com"
|
|
||||||
"user123@domain.com"
|
|
||||||
"USER@DOMAIN.COM"
|
|
||||||
"u@domain.io"
|
|
||||||
"user@sub.domain.com"
|
|
||||||
"user@domain.co.uk"
|
|
||||||
"user@domain.dev"
|
|
||||||
"a@bc.co"]]
|
|
||||||
(t/is (some? (spec/parse-email email)) (str "should accept: " email)))))
|
|
||||||
|
|
||||||
(t/deftest rejects-invalid-local-part
|
|
||||||
(t/testing "rejects local part starting with a dot"
|
|
||||||
(t/is (nil? (spec/parse-email ".user@domain.com"))))
|
|
||||||
|
|
||||||
(t/testing "rejects local part with consecutive dots"
|
|
||||||
(t/is (nil? (spec/parse-email "user..name@domain.com"))))
|
|
||||||
|
|
||||||
(t/testing "rejects local part with spaces"
|
|
||||||
(t/is (nil? (spec/parse-email "us er@domain.com"))))
|
|
||||||
|
|
||||||
(t/testing "rejects local part with comma"
|
|
||||||
(t/is (nil? (spec/parse-email "user,name@domain.com")))
|
|
||||||
(t/is (nil? (spec/parse-email ",user@domain.com"))))
|
|
||||||
|
|
||||||
(t/testing "rejects empty local part"
|
|
||||||
(t/is (nil? (spec/parse-email "@domain.com")))))
|
|
||||||
|
|
||||||
(t/deftest rejects-invalid-domain
|
|
||||||
(t/testing "rejects domain starting with a dot"
|
|
||||||
(t/is (nil? (spec/parse-email "user@.domain.com"))))
|
|
||||||
|
|
||||||
(t/testing "rejects domain part with comma"
|
|
||||||
(t/is (nil? (spec/parse-email "user@domain,com")))
|
|
||||||
(t/is (nil? (spec/parse-email "user@,domain.com"))))
|
|
||||||
|
|
||||||
(t/testing "rejects domain with consecutive dots"
|
|
||||||
(t/is (nil? (spec/parse-email "user@sub..domain.com"))))
|
|
||||||
|
|
||||||
(t/testing "rejects label starting with hyphen"
|
|
||||||
(t/is (nil? (spec/parse-email "user@-domain.com"))))
|
|
||||||
|
|
||||||
(t/testing "rejects label ending with hyphen"
|
|
||||||
(t/is (nil? (spec/parse-email "user@domain-.com"))))
|
|
||||||
|
|
||||||
(t/testing "rejects TLD shorter than 2 chars"
|
|
||||||
(t/is (nil? (spec/parse-email "user@domain.c"))))
|
|
||||||
|
|
||||||
(t/testing "rejects domain without a dot"
|
|
||||||
(t/is (nil? (spec/parse-email "user@domain"))))
|
|
||||||
|
|
||||||
(t/testing "rejects domain with spaces"
|
|
||||||
(t/is (nil? (spec/parse-email "user@do main.com"))))
|
|
||||||
|
|
||||||
(t/testing "rejects domain ending with a dot"
|
|
||||||
(t/is (nil? (spec/parse-email "user@domain.")))))
|
|
||||||
|
|
||||||
(t/deftest rejects-invalid-structure
|
|
||||||
(t/testing "rejects nil"
|
|
||||||
(t/is (nil? (spec/parse-email nil))))
|
|
||||||
|
|
||||||
(t/testing "rejects empty string"
|
|
||||||
(t/is (nil? (spec/parse-email ""))))
|
|
||||||
|
|
||||||
(t/testing "rejects string without @"
|
|
||||||
(t/is (nil? (spec/parse-email "userdomain.com"))))
|
|
||||||
|
|
||||||
(t/testing "rejects string with multiple @"
|
|
||||||
(t/is (nil? (spec/parse-email "user@@domain.com")))
|
|
||||||
(t/is (nil? (spec/parse-email "us@er@domain.com"))))
|
|
||||||
|
|
||||||
(t/testing "rejects empty domain"
|
|
||||||
(t/is (nil? (spec/parse-email "user@")))))
|
|
||||||
@ -6,13 +6,9 @@
|
|||||||
|
|
||||||
(ns common-tests.types.components-test
|
(ns common-tests.types.components-test
|
||||||
(:require
|
(:require
|
||||||
[app.common.test-helpers.components :as thc]
|
|
||||||
[app.common.test-helpers.compositions :as tho]
|
|
||||||
[app.common.test-helpers.files :as thf]
|
|
||||||
[app.common.test-helpers.ids-map :as thi]
|
[app.common.test-helpers.ids-map :as thi]
|
||||||
[app.common.test-helpers.shapes :as ths]
|
[app.common.test-helpers.shapes :as ths]
|
||||||
[app.common.types.component :as ctk]
|
[app.common.types.component :as ctk]
|
||||||
[app.common.types.file :as ctf]
|
|
||||||
[clojure.test :as t]))
|
[clojure.test :as t]))
|
||||||
|
|
||||||
(t/use-fixtures :each thi/test-fixture)
|
(t/use-fixtures :each thi/test-fixture)
|
||||||
@ -43,357 +39,3 @@
|
|||||||
(t/is (= (ctk/get-swap-slot s4) #uuid "9cc181fa-5eef-8084-8004-7bb2ab45fd1f"))
|
(t/is (= (ctk/get-swap-slot s4) #uuid "9cc181fa-5eef-8084-8004-7bb2ab45fd1f"))
|
||||||
(t/is (= (ctk/get-swap-slot s5) #uuid "9cc181fa-5eef-8084-8004-7bb2ab45fd1f"))
|
(t/is (= (ctk/get-swap-slot s5) #uuid "9cc181fa-5eef-8084-8004-7bb2ab45fd1f"))
|
||||||
(t/is (nil? (ctk/get-swap-slot s6)))))
|
(t/is (nil? (ctk/get-swap-slot s6)))))
|
||||||
|
|
||||||
(t/deftest test-find-near-match
|
|
||||||
|
|
||||||
(t/testing "shapes not in a component have no near match"
|
|
||||||
(let [file
|
|
||||||
;; :frame1 [:name Frame1]
|
|
||||||
;; :child1 [:name Rect1]
|
|
||||||
(-> (thf/sample-file :file1)
|
|
||||||
(tho/add-frame-with-child :frame1 :shape1))
|
|
||||||
|
|
||||||
page (thf/current-page file)
|
|
||||||
|
|
||||||
frame1 (ths/get-shape file :frame1)
|
|
||||||
shape1 (ths/get-shape file :shape1)
|
|
||||||
|
|
||||||
near-match1 (ctf/find-near-match file page {} frame1)
|
|
||||||
near-match2 (ctf/find-near-match file page {} shape1)]
|
|
||||||
|
|
||||||
(t/is (nil? near-match1))
|
|
||||||
(t/is (nil? near-match2))))
|
|
||||||
|
|
||||||
(t/testing "shapes in a copy get the ref-shape"
|
|
||||||
(let [file
|
|
||||||
;; {:main-root} [:name Frame1] # [Component :component1]
|
|
||||||
;; :main-child1 [:name Rect1]
|
|
||||||
;; :main-child2 [:name Rect2]
|
|
||||||
;; :main-child3 [:name Rect3]
|
|
||||||
;;
|
|
||||||
;; :copy-root [:name Frame1] #--> [Component :component1] :main-root
|
|
||||||
;; <no-label> [:name Rect1] ---> :main-child1
|
|
||||||
;; <no-label> [:name Rect2] ---> :main-child2
|
|
||||||
;; <no-label> [:name Rect3] ---> :main-child3
|
|
||||||
(-> (thf/sample-file :file1)
|
|
||||||
(tho/add-component-with-many-children-and-copy :component1
|
|
||||||
:main-root [:main-child1 :main-child2 :main-child3]
|
|
||||||
:copy-root))
|
|
||||||
|
|
||||||
page (thf/current-page file)
|
|
||||||
|
|
||||||
main-root (ths/get-shape file :main-root)
|
|
||||||
main-child1 (ths/get-shape file :main-child1)
|
|
||||||
main-child2 (ths/get-shape file :main-child2)
|
|
||||||
main-child3 (ths/get-shape file :main-child3)
|
|
||||||
copy-root (ths/get-shape file :copy-root)
|
|
||||||
copy-child1 (ths/get-shape-by-id file (nth (:shapes copy-root) 0))
|
|
||||||
copy-child2 (ths/get-shape-by-id file (nth (:shapes copy-root) 1))
|
|
||||||
copy-child3 (ths/get-shape-by-id file (nth (:shapes copy-root) 2))
|
|
||||||
|
|
||||||
near-main-root (ctf/find-near-match file page {} main-root)
|
|
||||||
near-main-child1 (ctf/find-near-match file page {} main-child1)
|
|
||||||
near-main-child2 (ctf/find-near-match file page {} main-child2)
|
|
||||||
near-main-child3 (ctf/find-near-match file page {} main-child3)
|
|
||||||
near-copy-root (ctf/find-near-match file page {} copy-root)
|
|
||||||
near-copy-child1 (ctf/find-near-match file page {} copy-child1)
|
|
||||||
near-copy-child2 (ctf/find-near-match file page {} copy-child2)
|
|
||||||
near-copy-child3 (ctf/find-near-match file page {} copy-child3)]
|
|
||||||
|
|
||||||
(t/is (nil? near-main-root))
|
|
||||||
(t/is (nil? near-main-child1))
|
|
||||||
(t/is (nil? near-main-child2))
|
|
||||||
(t/is (nil? near-main-child3))
|
|
||||||
(t/is (nil? near-copy-root))
|
|
||||||
(t/is (= (:id near-copy-child1) (thi/id :main-child1)))
|
|
||||||
(t/is (= (:id near-copy-child2) (thi/id :main-child2)))
|
|
||||||
(t/is (= (:id near-copy-child3) (thi/id :main-child3)))))
|
|
||||||
|
|
||||||
(t/testing "shapes in nested not swapped copies get the ref-shape"
|
|
||||||
(let [file
|
|
||||||
;; {:main1-root} [:name Frame1] # [Component :component1]
|
|
||||||
;; :main1-child [:name Rect1]
|
|
||||||
;;
|
|
||||||
;; {:main2-root} [:name Frame2] # [Component :component2]
|
|
||||||
;; :nested-head [:name Frame1] @--> [Component :component1] :main1-root
|
|
||||||
;; :nested-child [:name Rect1] ---> :main1-child
|
|
||||||
;;
|
|
||||||
;; :copy2 [:name Frame2] #--> [Component :component2] :main2-root
|
|
||||||
;; :copy2-nested-head [:name Frame1] @--> [Component :component1] :nested-head
|
|
||||||
;; :copy2-nested-child [:name Rect1] ---> :nested-child
|
|
||||||
(-> (thf/sample-file :file1)
|
|
||||||
(tho/add-nested-component :component1 :main1-root :main1-child
|
|
||||||
:component2 :main2-root :nested-head
|
|
||||||
:nested-head-params {:children-labels [:nested-child]})
|
|
||||||
(thc/instantiate-component :component2 :copy2
|
|
||||||
:children-labels [:copy2-nested-head :copy2-nested-child]))
|
|
||||||
|
|
||||||
page (thf/current-page file)
|
|
||||||
|
|
||||||
main1-root (ths/get-shape file :main1-root)
|
|
||||||
main1-child (ths/get-shape file :main1-child)
|
|
||||||
main2-root (ths/get-shape file :main2-root)
|
|
||||||
nested-head (ths/get-shape file :nested-head)
|
|
||||||
nested-child (ths/get-shape file :nested-child)
|
|
||||||
copy2 (ths/get-shape file :copy2)
|
|
||||||
copy2-nested-head (ths/get-shape file :copy2-nested-head)
|
|
||||||
copy2-nested-child (ths/get-shape file :copy2-nested-child)
|
|
||||||
|
|
||||||
near-main1-root (ctf/find-near-match file page {} main1-root)
|
|
||||||
near-main1-child (ctf/find-near-match file page {} main1-child)
|
|
||||||
near-main2-root (ctf/find-near-match file page {} main2-root)
|
|
||||||
near-nested-head (ctf/find-near-match file page {} nested-head)
|
|
||||||
near-nested-child (ctf/find-near-match file page {} nested-child)
|
|
||||||
near-copy2 (ctf/find-near-match file page {} copy2)
|
|
||||||
near-copy2-nested-head (ctf/find-near-match file page {} copy2-nested-head)
|
|
||||||
near-copy2-nested-child (ctf/find-near-match file page {} copy2-nested-child)]
|
|
||||||
|
|
||||||
(t/is (nil? near-main1-root))
|
|
||||||
(t/is (nil? near-main1-child))
|
|
||||||
(t/is (nil? near-main2-root))
|
|
||||||
(t/is (nil? near-nested-head))
|
|
||||||
(t/is (= (:id near-nested-child) (thi/id :main1-child)))
|
|
||||||
(t/is (nil? near-copy2))
|
|
||||||
(t/is (= (:id near-copy2-nested-head) (thi/id :nested-head)))
|
|
||||||
(t/is (= (:id near-copy2-nested-child) (thi/id :nested-child)))))
|
|
||||||
|
|
||||||
(t/testing "shapes in swapped copies get the swap slot"
|
|
||||||
(let [file
|
|
||||||
;; {:main1-root} [:name Frame1] # [Component :component1]
|
|
||||||
;; :main1-child [:name Rect1]
|
|
||||||
;;
|
|
||||||
;; {:main2-root} [:name Frame2] # [Component :component2]
|
|
||||||
;; :nested-head [:name Frame1] @--> [Component :component1] :main1-root
|
|
||||||
;; :nested-child [:name Rect1] ---> :main1-child
|
|
||||||
;;
|
|
||||||
;; {:main3-root} [:name Frame3] # [Component :component3]
|
|
||||||
;; :main3-child [:name Rect3]
|
|
||||||
;;
|
|
||||||
;; :copy2 [:name Frame2] #--> [Component :component2] :main2-root
|
|
||||||
;; :copy2-nested-head [:name Frame3] @--> [Component :component3] :main3-root
|
|
||||||
;; {swap-slot :nested-head}
|
|
||||||
;; <no-label> [:name Rect3] ---> :main3-child
|
|
||||||
(-> (thf/sample-file :file1)
|
|
||||||
(tho/add-nested-component :component1 :main1-root :main1-child
|
|
||||||
:component2 :main2-root :nested-head
|
|
||||||
:nested-head-params {:children-labels [:nested-child]})
|
|
||||||
(thc/instantiate-component :component2 :copy2 :children-labels [:copy2-nested-head])
|
|
||||||
(tho/add-simple-component :component3 :main3-root :main3-child
|
|
||||||
:root-params {:name "Frame3"}
|
|
||||||
:child-params {:name "Rect3"})
|
|
||||||
(tho/swap-component-in-first-child :copy2 :component3))
|
|
||||||
|
|
||||||
page (thf/current-page file)
|
|
||||||
|
|
||||||
main1-root (ths/get-shape file :main1-root)
|
|
||||||
main1-child (ths/get-shape file :main1-child)
|
|
||||||
main2-root (ths/get-shape file :main2-root)
|
|
||||||
nested-head (ths/get-shape file :nested-head)
|
|
||||||
nested-child (ths/get-shape file :nested-child)
|
|
||||||
copy2 (ths/get-shape file :copy2)
|
|
||||||
copy2-nested-head (ths/get-shape file :copy2-nested-head)
|
|
||||||
copy2-nested-child (ths/get-shape-by-id file (first (:shapes copy2-nested-head)))
|
|
||||||
|
|
||||||
near-main1-root (ctf/find-near-match file page {} main1-root)
|
|
||||||
near-main1-child (ctf/find-near-match file page {} main1-child)
|
|
||||||
near-main2-root (ctf/find-near-match file page {} main2-root)
|
|
||||||
near-nested-head (ctf/find-near-match file page {} nested-head)
|
|
||||||
near-nested-child (ctf/find-near-match file page {} nested-child)
|
|
||||||
near-copy2 (ctf/find-near-match file page {} copy2)
|
|
||||||
near-copy2-nested-head (ctf/find-near-match file page {} copy2-nested-head)
|
|
||||||
near-copy2-nested-child (ctf/find-near-match file page {} copy2-nested-child)]
|
|
||||||
|
|
||||||
(t/is (nil? near-main1-root))
|
|
||||||
(t/is (nil? near-main1-child))
|
|
||||||
(t/is (nil? near-main2-root))
|
|
||||||
(t/is (nil? near-nested-head))
|
|
||||||
(t/is (= (:id near-nested-child) (thi/id :main1-child)))
|
|
||||||
(t/is (nil? near-copy2))
|
|
||||||
(t/is (= (:id near-copy2-nested-head) (thi/id :nested-head)))
|
|
||||||
(t/is (= (:id near-copy2-nested-child) (thi/id :main3-child)))))
|
|
||||||
|
|
||||||
(t/testing "shapes in second level nested copies under swapped get the shape in the new main"
|
|
||||||
(let [file
|
|
||||||
;; {:main1-root} [:name Frame1] # [Component :component1]
|
|
||||||
;; :main1-child [:name Rect1]
|
|
||||||
;;
|
|
||||||
;; {:main2-root} [:name Frame2] # [Component :component2]
|
|
||||||
;; :nested2-head [:name Frame1] @--> [Component :component1] :main1-root
|
|
||||||
;; :nested2-child [:name Rect1] ---> :main1-child
|
|
||||||
;;
|
|
||||||
;; {:main3-root} [:name Frame3] # [Component :component3]
|
|
||||||
;; :main3-child [:name Rect3]
|
|
||||||
;;
|
|
||||||
;; {:main4-root} [:name Frame4] # [Component :component4]
|
|
||||||
;; :nested4-head [:name Frame3] @--> [Component :component1] :main3-root
|
|
||||||
;; :nested4-child [:name Rect3] ---> :main3-child
|
|
||||||
;;
|
|
||||||
;; :copy2 [:name Frame2] #--> [Component :component2] :main2-root
|
|
||||||
;; :copy2-nested-head [:name Frame4] @--> [Component :component4] :main4-root
|
|
||||||
;; {swap-slot :nested2-head}
|
|
||||||
;; <no-label> [:name Frame3] @--> :nested4-head
|
|
||||||
;; <no-label> [:name Rect3] ---> :nested4-child
|
|
||||||
(-> (thf/sample-file :file1)
|
|
||||||
(tho/add-nested-component :component1 :main1-root :main1-child
|
|
||||||
:component2 :main2-root :nested2-head
|
|
||||||
:nested-head-params {:children-labels [:nested2-child]})
|
|
||||||
(thc/instantiate-component :component2 :copy2 :children-labels [:copy2-nested-head])
|
|
||||||
(tho/add-nested-component :component3 :main3-root :main3-child
|
|
||||||
:component4 :main4-root :nested4-head
|
|
||||||
:root1-params {:name "Frame3"}
|
|
||||||
:main1-child-params {:name "Rect3"}
|
|
||||||
:main2-root-params {:name "Frame4"}
|
|
||||||
:nested-head-params {:children-labels [:nested4-child]})
|
|
||||||
(tho/swap-component-in-first-child :copy2 :component4))
|
|
||||||
|
|
||||||
page (thf/current-page file)
|
|
||||||
|
|
||||||
main1-root (ths/get-shape file :main1-root)
|
|
||||||
main1-child (ths/get-shape file :main1-child)
|
|
||||||
main2-root (ths/get-shape file :main2-root)
|
|
||||||
nested2-head (ths/get-shape file :nested2-head)
|
|
||||||
nested2-child (ths/get-shape file :nested2-child)
|
|
||||||
main3-root (ths/get-shape file :main3-root)
|
|
||||||
main3-child (ths/get-shape file :main3-child)
|
|
||||||
main4-root (ths/get-shape file :main4-root)
|
|
||||||
nested4-head (ths/get-shape file :nested4-head)
|
|
||||||
nested4-child (ths/get-shape file :nested4-child)
|
|
||||||
copy2 (ths/get-shape file :copy2)
|
|
||||||
copy2-nested-head (ths/get-shape file :copy2-nested-head)
|
|
||||||
copy2-nested4-head (ths/get-shape-by-id file (first (:shapes copy2-nested-head)))
|
|
||||||
copy2-nested4-child (ths/get-shape-by-id file (first (:shapes copy2-nested4-head)))
|
|
||||||
|
|
||||||
near-main1-root (ctf/find-near-match file page {} main1-root)
|
|
||||||
near-main1-child (ctf/find-near-match file page {} main1-child)
|
|
||||||
near-main2-root (ctf/find-near-match file page {} main2-root)
|
|
||||||
near-nested2-head (ctf/find-near-match file page {} nested2-head)
|
|
||||||
near-nested2-child (ctf/find-near-match file page {} nested2-child)
|
|
||||||
near-main3-root (ctf/find-near-match file page {} main3-root)
|
|
||||||
near-main3-child (ctf/find-near-match file page {} main3-child)
|
|
||||||
near-main4-root (ctf/find-near-match file page {} main4-root)
|
|
||||||
near-nested4-head (ctf/find-near-match file page {} nested4-head)
|
|
||||||
near-nested4-child (ctf/find-near-match file page {} nested4-child)
|
|
||||||
near-copy2 (ctf/find-near-match file page {} copy2)
|
|
||||||
near-copy2-nested-head (ctf/find-near-match file page {} copy2-nested-head)
|
|
||||||
near-copy2-nested4-head (ctf/find-near-match file page {} copy2-nested4-head)
|
|
||||||
near-copy2-nested4-child (ctf/find-near-match file page {} copy2-nested4-child)]
|
|
||||||
|
|
||||||
(t/is (nil? near-main1-root))
|
|
||||||
(t/is (nil? near-main1-child))
|
|
||||||
(t/is (nil? near-main2-root))
|
|
||||||
(t/is (nil? near-nested2-head))
|
|
||||||
(t/is (= (:id near-nested2-child) (thi/id :main1-child)))
|
|
||||||
(t/is (nil? near-main3-root))
|
|
||||||
(t/is (nil? near-main3-child))
|
|
||||||
(t/is (nil? near-main4-root))
|
|
||||||
(t/is (nil? near-nested4-head))
|
|
||||||
(t/is (= (:id near-nested4-child) (thi/id :main3-child)))
|
|
||||||
(t/is (nil? near-copy2))
|
|
||||||
(t/is (= (:id near-copy2-nested-head) (thi/id :nested2-head)))
|
|
||||||
(t/is (= (:id near-copy2-nested4-head) (thi/id :nested4-head)))
|
|
||||||
(t/is (= (:id near-copy2-nested4-child) (thi/id :nested4-child)))))
|
|
||||||
|
|
||||||
(t/testing "component in external libraries still work well"
|
|
||||||
(let [library1
|
|
||||||
;; {:main1-root} [:name Frame1] # [Component :component1]
|
|
||||||
;; :main1-child [:name Rect1]
|
|
||||||
;;
|
|
||||||
;; {:main2-root} [:name Frame2] # [Component :component2]
|
|
||||||
;; :nested2-head [:name Frame1] @--> [Component :component1] :main1-root
|
|
||||||
;; :nested2-child [:name Rect1] ---> :main1-child
|
|
||||||
(-> (thf/sample-file :library1)
|
|
||||||
(tho/add-nested-component :component1 :main1-root :main1-child
|
|
||||||
:component2 :main2-root :nested2-head
|
|
||||||
:nested-head-params {:children-labels [:nested2-child]}))
|
|
||||||
library2
|
|
||||||
;; {:main3-root} [:name Frame3] # [Component :component3]
|
|
||||||
;; :main3-child [:name Rect3]
|
|
||||||
;;
|
|
||||||
;; {:main4-root} [:name Frame4] # [Component :component4]
|
|
||||||
;; :nested4-head [:name Frame3] @--> [Component :component1] :main3-root
|
|
||||||
;; :nested4-child [:name Rect3] ---> :main3-child
|
|
||||||
(-> (thf/sample-file :library2)
|
|
||||||
(tho/add-nested-component :component3 :main3-root :main3-child
|
|
||||||
:component4 :main4-root :nested4-head
|
|
||||||
:root1-params {:name "Frame3"}
|
|
||||||
:main1-child-params {:name "Rect3"}
|
|
||||||
:main2-root-params {:name "Frame4"}
|
|
||||||
:nested-head-params {:children-labels [:nested4-child]}))
|
|
||||||
|
|
||||||
file
|
|
||||||
;; :copy2 [:name Frame2] #--> [Component :component2] :main2-root
|
|
||||||
;; :copy2-nested-head [:name Frame4] @--> [Component :component4] :main4-root
|
|
||||||
;; {swap-slot :nested2-head}
|
|
||||||
;; <no-label> [:name Frame3] @--> :nested4-head
|
|
||||||
;; <no-label> [:name Rect3] ---> :nested4-child
|
|
||||||
(-> (thf/sample-file :file1)
|
|
||||||
(thc/instantiate-component :component2 :copy2 :children-labels [:copy2-nested-head]
|
|
||||||
:library library1)
|
|
||||||
(tho/swap-component-in-first-child :copy2 :component4 :library library2))
|
|
||||||
|
|
||||||
page-library1 (thf/current-page library1)
|
|
||||||
page-library2 (thf/current-page library2)
|
|
||||||
page-file (thf/current-page file)
|
|
||||||
libraries {(:id library1) library1
|
|
||||||
(:id library2) library2}
|
|
||||||
|
|
||||||
main1-root (ths/get-shape library1 :main1-root)
|
|
||||||
main1-child (ths/get-shape library1 :main1-child)
|
|
||||||
main2-root (ths/get-shape library1 :main2-root)
|
|
||||||
nested2-head (ths/get-shape library1 :nested2-head)
|
|
||||||
nested2-child (ths/get-shape library1 :nested2-child)
|
|
||||||
main3-root (ths/get-shape library2 :main3-root)
|
|
||||||
main3-child (ths/get-shape library2 :main3-child)
|
|
||||||
main4-root (ths/get-shape library2 :main4-root)
|
|
||||||
nested4-head (ths/get-shape library2 :nested4-head)
|
|
||||||
nested4-child (ths/get-shape library2 :nested4-child)
|
|
||||||
copy2 (ths/get-shape file :copy2)
|
|
||||||
copy2-nested-head (ths/get-shape file :copy2-nested-head)
|
|
||||||
copy2-nested4-head (ths/get-shape-by-id file (first (:shapes copy2-nested-head)))
|
|
||||||
copy2-nested4-child (ths/get-shape-by-id file (first (:shapes copy2-nested4-head)))
|
|
||||||
|
|
||||||
near-main1-root (ctf/find-near-match file page-file libraries main1-root)
|
|
||||||
near-main1-child (ctf/find-near-match file page-file libraries main1-child)
|
|
||||||
near-main2-root (ctf/find-near-match file page-file libraries main2-root)
|
|
||||||
near-nested2-head (ctf/find-near-match library1 page-library1 libraries nested2-head)
|
|
||||||
near-nested2-child (ctf/find-near-match library1 page-library1 libraries nested2-child)
|
|
||||||
near-main3-root (ctf/find-near-match file page-file libraries main3-root)
|
|
||||||
near-main3-child (ctf/find-near-match file page-file libraries main3-child)
|
|
||||||
near-main4-root (ctf/find-near-match file page-file libraries main4-root)
|
|
||||||
near-nested4-head (ctf/find-near-match library2 page-library2 libraries nested4-head)
|
|
||||||
near-nested4-child (ctf/find-near-match library2 page-library2 libraries nested4-child)
|
|
||||||
near-copy2 (ctf/find-near-match file page-file libraries copy2)
|
|
||||||
near-copy2-nested-head (ctf/find-near-match file page-file libraries copy2-nested-head)
|
|
||||||
near-copy2-nested4-head (ctf/find-near-match file page-file libraries copy2-nested4-head)
|
|
||||||
near-copy2-nested4-child (ctf/find-near-match file page-file libraries copy2-nested4-child)]
|
|
||||||
|
|
||||||
(thf/dump-file library1 :keys [:name :swap-slot-label] :show-refs? true)
|
|
||||||
(t/is (some? main1-root))
|
|
||||||
(t/is (some? main1-child))
|
|
||||||
(t/is (some? main2-root))
|
|
||||||
(t/is (some? nested2-head))
|
|
||||||
(t/is (some? nested2-child))
|
|
||||||
(t/is (some? main3-root))
|
|
||||||
(t/is (some? main3-child))
|
|
||||||
(t/is (some? main4-root))
|
|
||||||
(t/is (some? nested4-head))
|
|
||||||
(t/is (some? nested4-child))
|
|
||||||
(t/is (some? copy2))
|
|
||||||
(t/is (some? copy2-nested-head))
|
|
||||||
(t/is (some? copy2-nested4-head))
|
|
||||||
(t/is (some? copy2-nested4-child))
|
|
||||||
|
|
||||||
(t/is (nil? near-main1-root))
|
|
||||||
(t/is (nil? near-main1-child))
|
|
||||||
(t/is (nil? near-main2-root))
|
|
||||||
(t/is (nil? near-nested2-head))
|
|
||||||
(t/is (= (:id near-nested2-child) (thi/id :main1-child)))
|
|
||||||
(t/is (nil? near-main3-root))
|
|
||||||
(t/is (nil? near-main3-child))
|
|
||||||
(t/is (nil? near-main4-root))
|
|
||||||
(t/is (nil? near-nested4-head))
|
|
||||||
(t/is (= (:id near-nested4-child) (thi/id :main3-child)))
|
|
||||||
(t/is (nil? near-copy2))
|
|
||||||
(t/is (= (:id near-copy2-nested-head) (thi/id :nested2-head)))
|
|
||||||
(t/is (= (:id near-copy2-nested4-head) (thi/id :nested4-head)))
|
|
||||||
(t/is (= (:id near-copy2-nested4-child) (thi/id :nested4-child))))))
|
|
||||||
|
|||||||
@ -32,7 +32,7 @@ RUN set -ex; \
|
|||||||
|
|
||||||
FROM base AS setup-node
|
FROM base AS setup-node
|
||||||
|
|
||||||
ENV NODE_VERSION=v24.15.0 \
|
ENV NODE_VERSION=v22.22.0 \
|
||||||
PATH=/opt/node/bin:$PATH
|
PATH=/opt/node/bin:$PATH
|
||||||
|
|
||||||
RUN set -eux; \
|
RUN set -eux; \
|
||||||
@ -67,7 +67,7 @@ RUN set -eux; \
|
|||||||
|
|
||||||
FROM base AS setup-caddy
|
FROM base AS setup-caddy
|
||||||
|
|
||||||
ENV CADDY_VERSION=2.11.2
|
ENV CADDY_VERSION=2.10.2
|
||||||
|
|
||||||
RUN set -eux; \
|
RUN set -eux; \
|
||||||
ARCH="$(dpkg --print-architecture)"; \
|
ARCH="$(dpkg --print-architecture)"; \
|
||||||
@ -99,18 +99,18 @@ RUN set -eux; \
|
|||||||
FROM base AS setup-jvm
|
FROM base AS setup-jvm
|
||||||
|
|
||||||
# https://clojure.org/releases/tools
|
# https://clojure.org/releases/tools
|
||||||
ENV CLOJURE_VERSION=1.12.4.1618
|
ENV CLOJURE_VERSION=1.12.4.1602
|
||||||
|
|
||||||
RUN set -eux; \
|
RUN set -eux; \
|
||||||
ARCH="$(dpkg --print-architecture)"; \
|
ARCH="$(dpkg --print-architecture)"; \
|
||||||
case "${ARCH}" in \
|
case "${ARCH}" in \
|
||||||
aarch64|arm64) \
|
aarch64|arm64) \
|
||||||
ESUM='cc1b459dc442d7422b46a3b5fe52acaea54879fa7913e29a05650cef54687f5f'; \
|
ESUM='9903c6b19183a33725ca1dfdae5b72400c9d00995c76fafc4a0d31c5152f33f7'; \
|
||||||
BINARY_URL='https://cdn.azul.com/zulu/bin/zulu26.30.11-ca-jdk26.0.1-linux_aarch64.tar.gz'; \
|
BINARY_URL='https://cdn.azul.com/zulu/bin/zulu25.32.21-ca-jdk25.0.2-linux_aarch64.tar.gz'; \
|
||||||
;; \
|
;; \
|
||||||
amd64|x86_64) \
|
amd64|x86_64) \
|
||||||
ESUM='7d6663ea8d4298df65de065e32f9f449745ff607d30ba5d13777cb92e9d4613d'; \
|
ESUM='946ad9766d98fc6ab495a1a120072197db54997f6925fb96680f1ecd5591db4e'; \
|
||||||
BINARY_URL='https://cdn.azul.com/zulu/bin/zulu26.30.11-ca-jdk26.0.1-linux_x64.tar.gz'; \
|
BINARY_URL='https://cdn.azul.com/zulu/bin/zulu25.32.21-ca-jdk25.0.2-linux_x64.tar.gz'; \
|
||||||
;; \
|
;; \
|
||||||
*) \
|
*) \
|
||||||
echo "Unsupported arch: ${ARCH}"; \
|
echo "Unsupported arch: ${ARCH}"; \
|
||||||
@ -181,10 +181,10 @@ RUN set -eux; \
|
|||||||
|
|
||||||
FROM base AS setup-utils
|
FROM base AS setup-utils
|
||||||
|
|
||||||
ENV CLJKONDO_VERSION=2026.04.15 \
|
ENV CLJKONDO_VERSION=2026.01.19 \
|
||||||
BABASHKA_VERSION=1.12.208 \
|
BABASHKA_VERSION=1.12.208 \
|
||||||
CLJFMT_VERSION=0.16.4 \
|
CLJFMT_VERSION=0.15.6 \
|
||||||
PIXI_VERSION=0.67.2
|
PIXI_VERSION=0.63.2
|
||||||
|
|
||||||
RUN set -ex; \
|
RUN set -ex; \
|
||||||
ARCH="$(dpkg --print-architecture)"; \
|
ARCH="$(dpkg --print-architecture)"; \
|
||||||
|
|||||||
@ -105,7 +105,7 @@ services:
|
|||||||
# - "traefik.http.routers.penpot-https.tls=true"
|
# - "traefik.http.routers.penpot-https.tls=true"
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
<< : [*penpot-flags, *penpot-http-body-size, *penpot-public-uri]
|
<< : [*penpot-flags, *penpot-http-body-size]
|
||||||
|
|
||||||
penpot-backend:
|
penpot-backend:
|
||||||
image: "penpotapp/backend:${PENPOT_VERSION:-latest}"
|
image: "penpotapp/backend:${PENPOT_VERSION:-latest}"
|
||||||
|
|||||||
@ -1,3 +1,2 @@
|
|||||||
// Frontend configuration
|
// Frontend configuration
|
||||||
//var penpotFlags = "";
|
//var penpotFlags = "";
|
||||||
//var penpotOIDCName = "";
|
|
||||||
|
|||||||
@ -19,22 +19,9 @@ update_flags() {
|
|||||||
-e "s|^//var penpotFlags = .*;|var penpotFlags = \"$PENPOT_FLAGS\";|g" \
|
-e "s|^//var penpotFlags = .*;|var penpotFlags = \"$PENPOT_FLAGS\";|g" \
|
||||||
"$1")" > "$1"
|
"$1")" > "$1"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -n "$PENPOT_PUBLIC_URI" ]; then
|
|
||||||
echo "var penpotPublicURI = \"$PENPOT_PUBLIC_URI\";" >> "$1";
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
update_oidc_name() {
|
|
||||||
if [ -n "$PENPOT_OIDC_NAME" ]; then
|
|
||||||
echo "$(sed \
|
|
||||||
-e "s|^//var penpotOIDCName = .*;|var penpotOIDCName = \"$PENPOT_OIDC_NAME\";|g" \
|
|
||||||
"$1")" > "$1"
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
update_flags /var/www/app/js/config.js
|
update_flags /var/www/app/js/config.js
|
||||||
update_oidc_name /var/www/app/js/config.js
|
|
||||||
|
|
||||||
#########################################
|
#########################################
|
||||||
## Nginx Config
|
## Nginx Config
|
||||||
@ -43,9 +30,8 @@ update_oidc_name /var/www/app/js/config.js
|
|||||||
export PENPOT_BACKEND_URI=${PENPOT_BACKEND_URI:-http://penpot-backend:6060}
|
export PENPOT_BACKEND_URI=${PENPOT_BACKEND_URI:-http://penpot-backend:6060}
|
||||||
export PENPOT_EXPORTER_URI=${PENPOT_EXPORTER_URI:-http://penpot-exporter:6061}
|
export PENPOT_EXPORTER_URI=${PENPOT_EXPORTER_URI:-http://penpot-exporter:6061}
|
||||||
export PENPOT_NITRATE_URI=${PENPOT_NITRATE_URI:-http://penpot-nitrate:3000}
|
export PENPOT_NITRATE_URI=${PENPOT_NITRATE_URI:-http://penpot-nitrate:3000}
|
||||||
export PENPOT_MCP_URI=${PENPOT_MCP_URI:-http://penpot-mcp}
|
|
||||||
export PENPOT_HTTP_SERVER_MAX_BODY_SIZE=${PENPOT_HTTP_SERVER_MAX_BODY_SIZE:-367001600} # Default to 350MiB
|
export PENPOT_HTTP_SERVER_MAX_BODY_SIZE=${PENPOT_HTTP_SERVER_MAX_BODY_SIZE:-367001600} # Default to 350MiB
|
||||||
envsubst "\$PENPOT_BACKEND_URI,\$PENPOT_EXPORTER_URI,\$PENPOT_NITRATE_URI,\$PENPOT_MCP_URI,\$PENPOT_HTTP_SERVER_MAX_BODY_SIZE" \
|
envsubst "\$PENPOT_BACKEND_URI,\$PENPOT_EXPORTER_URI,\$PENPOT_NITRATE_URI,\$PENPOT_HTTP_SERVER_MAX_BODY_SIZE" \
|
||||||
< /tmp/nginx.conf.template > /etc/nginx/nginx.conf
|
< /tmp/nginx.conf.template > /etc/nginx/nginx.conf
|
||||||
|
|
||||||
PENPOT_DEFAULT_INTERNAL_RESOLVER="$(awk 'BEGIN{ORS=" "} $1=="nameserver" { sub(/%.*$/,"",$2); print ($2 ~ ":")? "["$2"]": $2}' /etc/resolv.conf)"
|
PENPOT_DEFAULT_INTERNAL_RESOLVER="$(awk 'BEGIN{ORS=" "} $1=="nameserver" { sub(/%.*$/,"",$2); print ($2 ~ ":")? "["$2"]": $2}' /etc/resolv.conf)"
|
||||||
|
|||||||
@ -135,23 +135,6 @@ http {
|
|||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /mcp/ws {
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection 'upgrade';
|
|
||||||
proxy_pass $PENPOT_MCP_URI:4402;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /mcp/stream {
|
|
||||||
proxy_pass $PENPOT_MCP_URI:4401/mcp;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /mcp/sse {
|
|
||||||
proxy_pass $PENPOT_MCP_URI:4401/sse;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /readyz {
|
location /readyz {
|
||||||
access_log off;
|
access_log off;
|
||||||
proxy_pass $PENPOT_BACKEND_URI$request_uri;
|
proxy_pass $PENPOT_BACKEND_URI$request_uri;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user