mirror of
https://github.com/penpot/penpot.git
synced 2026-01-10 07:18:56 -05:00
Compare commits
311 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
633a7eac4e | ||
|
|
357fba5d2b | ||
|
|
b727f2fe1f | ||
|
|
17af55d3c8 | ||
|
|
2e5138eddc | ||
|
|
731c21f082 | ||
|
|
99d7672284 | ||
|
|
567fdd9619 | ||
|
|
6067e438a3 | ||
|
|
fc17a1742a | ||
|
|
f7f1598e71 | ||
|
|
8caf559a1a | ||
|
|
e927161ec1 | ||
|
|
ba387a892f | ||
|
|
18015bde4f | ||
|
|
8affefbbab | ||
|
|
0225919a45 | ||
|
|
0901807db8 | ||
|
|
625cbfc50a | ||
|
|
b2bc5aff68 | ||
|
|
337c61db2c | ||
|
|
5c2c96fc2e | ||
|
|
04c77a8532 | ||
|
|
ebcf5b3177 | ||
|
|
9d2117e2ac | ||
|
|
c1c22dc6c6 | ||
|
|
1e10e3818e | ||
|
|
802c67ace4 | ||
|
|
5c3709b5d8 | ||
|
|
626c65df02 | ||
|
|
f2f492bf3f | ||
|
|
40f69d320e | ||
|
|
1893cd306a | ||
|
|
096b685e2c | ||
|
|
1965490bee | ||
|
|
559dcabf0e | ||
|
|
a9e8d8f8f7 | ||
|
|
dba67eea91 | ||
|
|
70fe6fda83 | ||
|
|
5155cf2b23 | ||
|
|
5ca9b95cca | ||
|
|
fa0da3a695 | ||
|
|
fa9d8a9b15 | ||
|
|
7403f60366 | ||
|
|
66295b0adf | ||
|
|
eb6d2fb0eb | ||
|
|
a8c34ccc1a | ||
|
|
8c501db2fa | ||
|
|
d2fbb9dfa7 | ||
|
|
e4c9b736f7 | ||
|
|
f02f446015 | ||
|
|
05d6d2fcd4 | ||
|
|
d5492442fb | ||
|
|
61800d8945 | ||
|
|
f450c9dbe3 | ||
|
|
b46574bef6 | ||
|
|
e3b3fa3342 | ||
|
|
21b2c0c26a | ||
|
|
dcbf54fae1 | ||
|
|
2fe6fb28e4 | ||
|
|
86022a967c | ||
|
|
0efbebd94f | ||
|
|
2aee2ea79e | ||
|
|
60a20b6984 | ||
|
|
fd753fb262 | ||
|
|
0ae57a017e | ||
|
|
88772a9ced | ||
|
|
65647f4aae | ||
|
|
5e6ccc44fc | ||
|
|
b9df8ad038 | ||
|
|
474cd1e55a | ||
|
|
b52e8bc87c | ||
|
|
f35723e772 | ||
|
|
415d1a2668 | ||
|
|
b3feb9bffd | ||
|
|
dfbf0d34b6 | ||
|
|
0c3fd8a6d9 | ||
|
|
5b9dd96e02 | ||
|
|
46c89a1bcf | ||
|
|
721760d679 | ||
|
|
2cdb874484 | ||
|
|
e5bccc470b | ||
|
|
ba768f8744 | ||
|
|
a33828467f | ||
|
|
6fed0f3b58 | ||
|
|
5f3599eaa7 | ||
|
|
44ca01aa27 | ||
|
|
451306f719 | ||
|
|
29518f3ba5 | ||
|
|
d74bfd834d | ||
|
|
ac8b5a7bcc | ||
|
|
390cf6b642 | ||
|
|
0dbf00a767 | ||
|
|
a361e0b990 | ||
|
|
3a8ba4cbee | ||
|
|
d5c9e68a3e | ||
|
|
253d94c176 | ||
|
|
fd941e4701 | ||
|
|
a99198de48 | ||
|
|
e729e85c42 | ||
|
|
7eb9325047 | ||
|
|
ba4554da79 | ||
|
|
97fb1e00c2 | ||
|
|
3eb332f3d0 | ||
|
|
0d11bafb57 | ||
|
|
e01dfd76e8 | ||
|
|
854145e435 | ||
|
|
707bfd4241 | ||
|
|
02bc6e62e7 | ||
|
|
9fde4e2121 | ||
|
|
66eb4fb5ad | ||
|
|
e362f423c0 | ||
|
|
dc08eb7899 | ||
|
|
a1e307b4ce | ||
|
|
a0f16fc038 | ||
|
|
7c36c76b0d | ||
|
|
8488be311e | ||
|
|
2297862d81 | ||
|
|
539fdfa016 | ||
|
|
a7f6797499 | ||
|
|
cc7f745a0a | ||
|
|
265675795e | ||
|
|
2c789e48f3 | ||
|
|
9da6c50cbe | ||
|
|
5c53de8e76 | ||
|
|
3cdc826fca | ||
|
|
02292f99ab | ||
|
|
0279e75c4b | ||
|
|
44f1798dce | ||
|
|
23468a9908 | ||
|
|
8eb2aaa0a8 | ||
|
|
aa468e2153 | ||
|
|
9e5de82967 | ||
|
|
856e2be1ca | ||
|
|
8d4b023d61 | ||
|
|
f06f11ad7a | ||
|
|
c807e37525 | ||
|
|
c9a8d2bd23 | ||
|
|
65c6c821e7 | ||
|
|
6cccacaaab | ||
|
|
7da97d69b0 | ||
|
|
f7574009b5 | ||
|
|
0416e883ca | ||
|
|
c0ccb86e3a | ||
|
|
7885f413b8 | ||
|
|
dfd5c5b508 | ||
|
|
bffbccac50 | ||
|
|
12c2d73846 | ||
|
|
27d15763f8 | ||
|
|
0eaa43f36b | ||
|
|
e30834bb2d | ||
|
|
fefb946a25 | ||
|
|
2d857ecf2f | ||
|
|
2cf179ccf6 | ||
|
|
5ebfc603e6 | ||
|
|
80d5272248 | ||
|
|
b4f6177be7 | ||
|
|
3907a1783a | ||
|
|
b676ea7127 | ||
|
|
ca65f5ad9a | ||
|
|
3e89b73ca0 | ||
|
|
17e9e836f6 | ||
|
|
c48d862d0f | ||
|
|
052282cff9 | ||
|
|
6d40166de7 | ||
|
|
ab7781b4fa | ||
|
|
88669d2e0f | ||
|
|
1187d64f69 | ||
|
|
3074fc9ab5 | ||
|
|
bcea19001e | ||
|
|
1c98c53805 | ||
|
|
4799f6fe0a | ||
|
|
7470fb709f | ||
|
|
8c1e18b1cd | ||
|
|
9143187efd | ||
|
|
b661f39422 | ||
|
|
3a764a9da6 | ||
|
|
2341dfb95d | ||
|
|
ff121d2af5 | ||
|
|
55d7bab0e6 | ||
|
|
4a3d951329 | ||
|
|
014c297458 | ||
|
|
280da72e63 | ||
|
|
6277db8d45 | ||
|
|
60530a80d9 | ||
|
|
d79fac7729 | ||
|
|
195127b099 | ||
|
|
a3e74c55f1 | ||
|
|
e408bc9113 | ||
|
|
89dc917cb9 | ||
|
|
f83cdf2f5d | ||
|
|
f3040fc10d | ||
|
|
44acd79081 | ||
|
|
a3e4da0b3d | ||
|
|
73a52e5395 | ||
|
|
6cb1aa24cd | ||
|
|
f5c913d26e | ||
|
|
c86f14e75d | ||
|
|
65a97167de | ||
|
|
0530c57d31 | ||
|
|
adbe29e3d1 | ||
|
|
5fae07af11 | ||
|
|
400e5f60f2 | ||
|
|
3268225941 | ||
|
|
91fa39705d | ||
|
|
c41f86f0a4 | ||
|
|
81b741478a | ||
|
|
f6fc2f8808 | ||
|
|
aa180e9f3f | ||
|
|
6b773d6b74 | ||
|
|
6cbaacf1e0 | ||
|
|
2ffb77cb4d | ||
|
|
59a57d6c3f | ||
|
|
8a332c1402 | ||
|
|
37855bfe7f | ||
|
|
f68b0117c4 | ||
|
|
aa867adbd3 | ||
|
|
3fd429c72a | ||
|
|
39bbb4c2bd | ||
|
|
64e6d0b1f8 | ||
|
|
26a2ef8fb7 | ||
|
|
a6c46ee55c | ||
|
|
f8d58cb74e | ||
|
|
3ea52a0198 | ||
|
|
054efb3435 | ||
|
|
773b4fe02e | ||
|
|
1a62e5e42d | ||
|
|
f812460158 | ||
|
|
654c070976 | ||
|
|
e776ba1b33 | ||
|
|
7497371b32 | ||
|
|
50afc4c507 | ||
|
|
064e51d24e | ||
|
|
a029ec18a6 | ||
|
|
b0a6e5c946 | ||
|
|
e8c85d13ff | ||
|
|
b228438127 | ||
|
|
1fb48a1e8a | ||
|
|
91b6d498fe | ||
|
|
efe204c346 | ||
|
|
4a4cd9492a | ||
|
|
5446464d7e | ||
|
|
baa5258a43 | ||
|
|
df6a679548 | ||
|
|
3692f17e55 | ||
|
|
2bac94ad5c | ||
|
|
d46d80bd5c | ||
|
|
f0e5196659 | ||
|
|
05a459ea19 | ||
|
|
8e85d5a02a | ||
|
|
81036b9330 | ||
|
|
c91b7606a0 | ||
|
|
d6e7a331d5 | ||
|
|
831b0baddd | ||
|
|
e4bf2bd9ad | ||
|
|
7a4d8b824e | ||
|
|
ff34d1d5f9 | ||
|
|
c1ce24e5f0 | ||
|
|
1f450c83ec | ||
|
|
769000da2d | ||
|
|
a53c37bc3c | ||
|
|
333cc5996c | ||
|
|
bccc90f5a2 | ||
|
|
5575a66b8d | ||
|
|
0fee8143dd | ||
|
|
b6e26d15e1 | ||
|
|
7e0b2702de | ||
|
|
e46fb9dba7 | ||
|
|
f4dee75a17 | ||
|
|
6d1ff0cb49 | ||
|
|
3dcabc9502 | ||
|
|
10174aa7bc | ||
|
|
cd87cbe44e | ||
|
|
4594c7bf0a | ||
|
|
d1a1dafcad | ||
|
|
246463a3ec | ||
|
|
4b4541515c | ||
|
|
1bb337c3dd | ||
|
|
8b380a01e6 | ||
|
|
5c32ec8cfa | ||
|
|
9660307f00 | ||
|
|
a3a757f842 | ||
|
|
a2727a110e | ||
|
|
dd1aba0d05 | ||
|
|
6692f8dce2 | ||
|
|
372b3145ea | ||
|
|
8b7a102927 | ||
|
|
24e1cf0d7d | ||
|
|
2ce88283a2 | ||
|
|
2ae2e23b57 | ||
|
|
9c7bb96b1c | ||
|
|
0f49208040 | ||
|
|
8f11a925df | ||
|
|
f65f7d68e6 | ||
|
|
f5b18f953d | ||
|
|
73ff1b4fe5 | ||
|
|
0c275cf490 | ||
|
|
8b466ef0a3 | ||
|
|
33887711f7 | ||
|
|
947bd547aa | ||
|
|
86e0f8ad34 | ||
|
|
48acc8715b | ||
|
|
9db76f9a15 | ||
|
|
df0909483e | ||
|
|
ec8109644b | ||
|
|
315b389a66 | ||
|
|
bce30eb522 | ||
|
|
d05d1c6a48 | ||
|
|
1803e32322 | ||
|
|
24281b512e | ||
|
|
f4f0478975 |
@@ -263,6 +263,12 @@ jobs:
|
||||
command: |
|
||||
cargo fmt --check
|
||||
|
||||
- run:
|
||||
name: "lint"
|
||||
working_directory: "./render-wasm"
|
||||
command: |
|
||||
./lint
|
||||
|
||||
- run:
|
||||
name: "cargo tests"
|
||||
working_directory: "./render-wasm"
|
||||
|
||||
36
.github/PULL_REQUEST_TEMPLATE.md
vendored
36
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,29 +1,19 @@
|
||||
<!--
|
||||
### Related Ticket
|
||||
|
||||
Some key notes before you open a PR:
|
||||
<!-- Reference the related GitHub/Taiga ticket. -->
|
||||
|
||||
1. Select which branch should this PR be merged in? By default, you should always merge to the develop branch.
|
||||
2. PR name follows [convention](http://karma-runner.github.io/4.0/dev/git-commit-msg.html)
|
||||
3. All tests pass locally, UI and Unit tests
|
||||
4. All business logic and validations must be on the server-side
|
||||
5. Update necessary Documentation
|
||||
6. Put `closes #XXXX` in your comment to auto-close the issue that your PR fixes
|
||||
### Summary
|
||||
|
||||
### Steps to reproduce
|
||||
|
||||
Also, if you're new here
|
||||
### Checklist
|
||||
|
||||
- Contribution Guide => https://github.com/uxbox/uxbox/blob/develop/CONTRIBUTING.md
|
||||
- [ ] Choose the correct target branch; use `develop` by default.
|
||||
- [ ] Provide a brief summary of the changes introduced.
|
||||
- [ ] Add a detailed explanation of how to reproduce the issue and/or verify the fix, if applicable.
|
||||
- [ ] Include screenshots or videos, if applicable.
|
||||
- [ ] Add or modify existing integration tests in case of bugs or new features, if applicable.
|
||||
- [ ] Check CI passes successfully.
|
||||
- [ ] Update the `CHANGES.md` file, referencing the related GitHub issue, if applicable.
|
||||
|
||||
-->
|
||||
|
||||
> Please provide enough information so that others can review your pull request:
|
||||
|
||||
<!-- You can skip this if you're fixing a typo or updating existing documentation -->
|
||||
|
||||
> Explain the **details** for making this change. What existing problem does the pull request solve?
|
||||
|
||||
<!-- Example: When "Adding a function to do X", explain why it is necessary to have a way to do X. -->
|
||||
|
||||
> Screenshots/GIFs
|
||||
|
||||
<!-- Add images/recordings to better visualize the change: expected/current behviour -->
|
||||
<!-- For more details, check the contribution guidelines: https://github.com/penpot/penpot/blob/develop/CONTRIBUTING.md -->
|
||||
|
||||
55
CHANGES.md
55
CHANGES.md
@@ -1,6 +1,6 @@
|
||||
# CHANGELOG
|
||||
|
||||
## 2.5.3
|
||||
## 2.6.0 (Unreleased)
|
||||
|
||||
### :rocket: Epics and highlights
|
||||
|
||||
@@ -10,19 +10,46 @@
|
||||
|
||||
### :sparkles: New features
|
||||
|
||||
- [COMMENTS] "Mark All as Read" Functionality in Dashboard [Taiga #9235](https://tree.taiga.io/project/penpot/us/9235)
|
||||
- [COMMENTS] Bubble Groups [Taiga #9236](https://tree.taiga.io/project/penpot/us/9236)
|
||||
- Change templates carrousel [Taiga #9803](https://tree.taiga.io/project/penpot/us/9803)
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix opacity in frame containers [Github #5858](https://github.com/penpot/penpot/pull/5858)
|
||||
- Avoid resizing on click [Taiga #10213](https://tree.taiga.io/project/penpot/issue/10213)
|
||||
- Hide horizontal scroll from dashboard sidebar [Taiga #10422](https://tree.taiga.io/project/penpot/issue/10422)
|
||||
- Fix cut and paste a copy a cmponent inside its parent [Taiga #10365](https://tree.taiga.io/project/penpot/us/10365)
|
||||
- Fix duplicate page with component over frame [Taiga #8151](https://tree.taiga.io/project/penpot/issue/8151) and [Taiga #9698](https://tree.taiga.io/project/penpot/issue/9698)
|
||||
- The plugin list in the navigation menu lacks scrolling, some plugins are not visible when a large number are installed [Taiga #9360](https://tree.taiga.io/project/penpot/us/9360)
|
||||
- Fix hidden toolbar click event still available [Taiga #10437](https://tree.taiga.io/project/penpot/us/10437)
|
||||
|
||||
|
||||
## 2.5.4
|
||||
|
||||
### :sparkles: New features
|
||||
|
||||
- Add support for WEBP format on shape export [Github #6053](https://github.com/penpot/penpot/pull/6053) and [Github #6074](https://github.com/penpot/penpot/pull/6074)
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix feature loading on workspace when opening a file in a background
|
||||
tab [Taiga #10377](https://tree.taiga.io/project/penpot/issue/10377)
|
||||
- Fix minor inconsistencies on RPC `get-file-libraries` and `get-file`
|
||||
methods (add missing team-id prop)
|
||||
- Fix problem with viewer role and inspect mode [Taiga #9751](https://tree.taiga.io/project/penpot/issue/9751)
|
||||
- Fix error when clicking on a comment at the viewer's sidebar [Taiga #10465](https://tree.taiga.io/project/penpot/issue/10465)
|
||||
|
||||
## 2.5.3
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Component sync issues with multiple tabs [Taiga #10471](https://tree.taiga.io/project/penpot/issue/10471)
|
||||
|
||||
## 2.5.2
|
||||
|
||||
### :rocket: Epics and highlights
|
||||
|
||||
### :boom: Breaking changes & Deprecations
|
||||
|
||||
### :heart: Community contributions (Thank you!)
|
||||
|
||||
### :sparkles: New features
|
||||
|
||||
- When the workspace is empty, set default the board creation tool [Taiga #9425](https://tree.taiga.io/project/penpot/us/9425)
|
||||
|
||||
### :bug: Bugs fixed
|
||||
@@ -34,22 +61,12 @@
|
||||
|
||||
## 2.5.1
|
||||
|
||||
### :rocket: Epics and highlights
|
||||
|
||||
### :boom: Breaking changes & Deprecations
|
||||
|
||||
### :heart: Community contributions (Thank you!)
|
||||
|
||||
### :sparkles: New features
|
||||
|
||||
- Improve Nginx entryponit to get the resolvers dinamically by default
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
## 2.5.0
|
||||
|
||||
### :rocket: Epics and highlights
|
||||
|
||||
### :boom: Breaking changes & Deprecations
|
||||
|
||||
Although this is not a breaking change, we believe it’s important to highlight it in this
|
||||
@@ -78,9 +95,6 @@ If you have a big database and many cores available, you can reduce the time of
|
||||
all files by increasing paralelizacion changing the `max-jobs` value from 1 to N (where N
|
||||
is a number of cores)
|
||||
|
||||
|
||||
### :heart: Community contributions (Thank you!)
|
||||
|
||||
### :sparkles: New features
|
||||
|
||||
- [GRADIENTS] New gradients UI with multi-stop support. [Taiga #3418](https://tree.taiga.io/project/penpot/epic/3418)
|
||||
@@ -97,6 +111,7 @@ is a number of cores)
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix menu shadow color [Taiga #10102](https://tree.taiga.io/project/penpot/issue/10102)
|
||||
- Fix missing state refresh on notifications update [Taiga #10253](https://tree.taiga.io/project/penpot/issue/10253)
|
||||
- Fix icon visualization on select component [Taiga #8889](https://tree.taiga.io/project/penpot/issue/8889)
|
||||
- Fix typo on integration tests docs [Taiga #10112](https://tree.taiga.io/project/penpot/issue/10112)
|
||||
|
||||
@@ -151,6 +151,78 @@ Debug Main Page
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row">
|
||||
<input type="submit" value="Submit" />
|
||||
</div>
|
||||
</form>
|
||||
</fieldset>
|
||||
</section>
|
||||
|
||||
<section class="widget">
|
||||
<h2>Feature Flags</h2>
|
||||
<fieldset>
|
||||
<legend>Enable</legend>
|
||||
<desc>Add a feature flag to a team</desc>
|
||||
<form method="post" action="/dbg/actions/add-team-feature">
|
||||
<div class="row">
|
||||
<input type="text" style="width:300px" name="team-id" placeholder="team-id" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<input type="text" style="width:100px" name="feature" placeholder="feature" value="" />
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label for="check-feature">Skip feature check</label>
|
||||
<input id="check-feature" type="checkbox" name="skip-check" />
|
||||
<br />
|
||||
<small>
|
||||
Do not check if the feature is supported
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label for="force-version">Are you sure?</label>
|
||||
<input id="force-version" type="checkbox" name="force" />
|
||||
<br />
|
||||
<small>
|
||||
This is a just a security double check for prevent non intentional submits.
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<input type="submit" value="Submit" />
|
||||
</div>
|
||||
</form>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>Disable</legend>
|
||||
<desc>Remove a feature flag from a team</desc>
|
||||
<form method="post" action="/dbg/actions/remove-team-feature">
|
||||
<div class="row">
|
||||
<input type="text" style="width:300px" name="team-id" placeholder="team-id" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<input type="text" style="width:100px" name="feature" placeholder="feature" value="" />
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label for="check-feature">Skip feature check</label>
|
||||
<input id="check-feature" type="checkbox" name="skip-check" />
|
||||
<br />
|
||||
<small>
|
||||
Do not check if the feature is supported
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label for="force-version">Are you sure?</label>
|
||||
<input id="force-version" type="checkbox" name="force" />
|
||||
<br />
|
||||
<small>
|
||||
This is a just a security double check for prevent non intentional submits.
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<input type="submit" value="Submit" />
|
||||
</div>
|
||||
|
||||
@@ -39,6 +39,11 @@
|
||||
"fdata/shape-data-type"
|
||||
nil
|
||||
|
||||
;; There is no migration needed, but we don't want to allow
|
||||
;; copy paste nor import of variant files into no-variant teams
|
||||
"variants/v1"
|
||||
nil
|
||||
|
||||
(ex/raise :type :internal
|
||||
:code :no-migration-defined
|
||||
:hint (str/ffmt "no migation for feature '%' on file importation" feature)
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
[clojure.set :as set]
|
||||
[integrant.core :as ig]
|
||||
[next.jdbc :as jdbc]
|
||||
[next.jdbc.date-time :as jdbc-dt])
|
||||
[next.jdbc.date-time :as jdbc-dt]
|
||||
[next.jdbc.transaction])
|
||||
(:import
|
||||
com.zaxxer.hikari.HikariConfig
|
||||
com.zaxxer.hikari.HikariDataSource
|
||||
@@ -223,16 +224,6 @@
|
||||
(let [^OutputStream os (.getOutputStream ^LargeObject lobj)]
|
||||
(io/make-output-stream os opts))))
|
||||
|
||||
(defmacro with-atomic
|
||||
[& args]
|
||||
(if (symbol? (first args))
|
||||
(let [cfgs (first args)
|
||||
body (rest args)]
|
||||
`(jdbc/with-transaction [conn# (::pool ~cfgs)]
|
||||
(let [~cfgs (assoc ~cfgs ::conn conn#)]
|
||||
~@body)))
|
||||
`(jdbc/with-transaction ~@args)))
|
||||
|
||||
(defn open
|
||||
[system-or-pool]
|
||||
(if (pool? system-or-pool)
|
||||
@@ -535,43 +526,31 @@
|
||||
(l/trc :hint "explicit rollback requested (savepoint)")
|
||||
(.rollback conn sp))))
|
||||
|
||||
(defn transact!
|
||||
"A lower-level function for executing function in a transaction"
|
||||
([transactable f] (transact! transactable f {}))
|
||||
([transactable f opts]
|
||||
(binding [next.jdbc.transaction/*nested-tx* :ignore]
|
||||
(jdbc/transact transactable f opts))))
|
||||
|
||||
(defn tx-run!
|
||||
"Run a function in a transaction."
|
||||
[system f & params]
|
||||
(cond
|
||||
(connection? system)
|
||||
(if (connection? system)
|
||||
(tx-run! {::conn system} f)
|
||||
|
||||
(pool? system)
|
||||
(tx-run! {::pool system} f)
|
||||
|
||||
(::conn system)
|
||||
(let [conn (::conn system)
|
||||
sp (savepoint conn)]
|
||||
(try
|
||||
(let [system' (-> system
|
||||
(assoc ::savepoint sp)
|
||||
(dissoc ::rollback))
|
||||
result (apply f system' params)]
|
||||
(if (::rollback system)
|
||||
(rollback! conn sp)
|
||||
(release! conn sp))
|
||||
result)
|
||||
(catch Throwable cause
|
||||
(.rollback ^Connection conn ^Savepoint sp)
|
||||
(throw cause))))
|
||||
|
||||
(::pool system)
|
||||
(with-atomic [conn (::pool system)]
|
||||
(let [system' (-> system
|
||||
(assoc ::conn conn)
|
||||
(dissoc ::rollback))
|
||||
result (apply f system' params)]
|
||||
(when (::rollback system)
|
||||
(rollback! conn))
|
||||
result))
|
||||
|
||||
:else
|
||||
(throw (IllegalArgumentException. "invalid system/cfg provided"))))
|
||||
(if (pool? system)
|
||||
(tx-run! {::pool system} f)
|
||||
(if-let [conn (or (::conn system)
|
||||
(::pool system))]
|
||||
(transact! conn
|
||||
(fn [conn]
|
||||
(let [system' (-> system
|
||||
(dissoc ::rollback)
|
||||
(assoc ::conn conn))]
|
||||
(apply f system' params)))
|
||||
{:rollback-only (::rollback system)
|
||||
:read-only (::read-only system)})
|
||||
(throw (IllegalArgumentException. "invalid system/cfg provided"))))))
|
||||
|
||||
(defn run!
|
||||
[system f & params]
|
||||
|
||||
@@ -1071,7 +1071,7 @@
|
||||
groups (d/group-by #(first (cfh/split-path (:path %))) assets)
|
||||
;; If there is a group called as the generic-name we have to preserve it
|
||||
unames (into #{} (keep str) (keys groups))
|
||||
groups (rename-keys groups {generic-name (cfh/generate-unique-name unames generic-name)})
|
||||
groups (rename-keys groups {generic-name (cfh/generate-unique-name generic-name unames)})
|
||||
|
||||
;; Split large groups in chunks of max-group-size elements
|
||||
groups (loop [groups (seq groups)
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
[app.rpc.commands.profile :as profile]
|
||||
[app.rpc.commands.teams :as teams]
|
||||
[app.setup :as-alias setup]
|
||||
[app.srepl.helpers :as srepl]
|
||||
[app.srepl.main :as srepl]
|
||||
[app.storage :as-alias sto]
|
||||
[app.storage.tmp :as tmp]
|
||||
[app.util.blob :as blob]
|
||||
@@ -430,6 +430,50 @@
|
||||
::yres/body "OK"}))
|
||||
|
||||
|
||||
(defn- add-team-feature
|
||||
[{:keys [params] :as request}]
|
||||
(let [team-id (some-> params :team-id d/parse-uuid)
|
||||
feature (some-> params :feature str)
|
||||
skip-check (contains? params :skip-check)]
|
||||
|
||||
(when-not (contains? params :force)
|
||||
(ex/raise :type :validation
|
||||
:code :missing-force
|
||||
:hint "missing force checkbox"))
|
||||
|
||||
(when (nil? team-id)
|
||||
(ex/raise :type :validation
|
||||
:code :invalid-team-id
|
||||
:hint "provided invalid team id"))
|
||||
|
||||
(srepl/enable-team-feature! team-id feature :skip-check skip-check)
|
||||
|
||||
{::yres/status 200
|
||||
::yres/headers {"content-type" "text/plain"}
|
||||
::yres/body "OK"}))
|
||||
|
||||
(defn- remove-team-feature
|
||||
[{:keys [params] :as request}]
|
||||
(let [team-id (some-> params :team-id d/parse-uuid)
|
||||
feature (some-> params :feature str)
|
||||
skip-check (contains? params :skip-check)]
|
||||
|
||||
(when-not (contains? params :force)
|
||||
(ex/raise :type :validation
|
||||
:code :missing-force
|
||||
:hint "missing force checkbox"))
|
||||
|
||||
(when (nil? team-id)
|
||||
(ex/raise :type :validation
|
||||
:code :invalid-team-id
|
||||
:hint "provided invalid team id"))
|
||||
|
||||
(srepl/disable-team-feature! team-id feature :skip-check skip-check)
|
||||
|
||||
{::yres/status 200
|
||||
::yres/headers {"content-type" "text/plain"}
|
||||
::yres/body "OK"}))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; OTHER SMALL VIEWS/HANDLERS
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
@@ -500,6 +544,10 @@
|
||||
{:handler (partial resend-email-notification cfg)}]
|
||||
["/actions/reset-file-version"
|
||||
{:handler (partial reset-file-version cfg)}]
|
||||
["/actions/add-team-feature"
|
||||
{:handler (partial add-team-feature)}]
|
||||
["/actions/remove-team-feature"
|
||||
{:handler (partial remove-team-feature)}]
|
||||
["/file/export" {:handler (partial export-handler cfg)}]
|
||||
["/file/import" {:handler (partial import-handler cfg)}]
|
||||
["/file/data" {:handler (partial file-data-handler cfg)}]
|
||||
|
||||
@@ -337,16 +337,17 @@
|
||||
or (updated_at is null and
|
||||
created_at < now() - ?::interval)")
|
||||
|
||||
(defmethod ig/init-key ::tasks/gc
|
||||
[_ {:keys [::db/pool ::tasks/max-age] :as cfg}]
|
||||
(l/debug :hint "initializing session gc task" :max-age max-age)
|
||||
(fn [_]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [interval (db/interval max-age)
|
||||
result (db/exec-one! conn [sql:delete-expired interval interval])
|
||||
result (:next.jdbc/update-count result)]
|
||||
(l/debug :task "gc"
|
||||
:hint "clean http sessions"
|
||||
:deleted result)
|
||||
result))))
|
||||
(defn- collect-expired-tasks
|
||||
[{:keys [::db/conn ::tasks/max-age]}]
|
||||
(let [interval (db/interval max-age)
|
||||
result (db/exec-one! conn [sql:delete-expired interval interval])
|
||||
result (:next.jdbc/update-count result)]
|
||||
(l/debug :task "gc"
|
||||
:hint "clean http sessions"
|
||||
:deleted result)
|
||||
result))
|
||||
|
||||
(defmethod ig/init-key ::tasks/gc
|
||||
[_ {:keys [::tasks/max-age] :as cfg}]
|
||||
(l/debug :hint "initializing session gc task" :max-age max-age)
|
||||
(fn [_] (db/tx-run! cfg collect-expired-tasks)))
|
||||
|
||||
@@ -43,13 +43,8 @@
|
||||
(decode-row token)))
|
||||
|
||||
(defn repl:create-access-token
|
||||
[{:keys [::db/pool] :as system} profile-id name expiration]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [props (:app.setup/props system)]
|
||||
(create-access-token {::db/conn conn ::setup/props props}
|
||||
profile-id
|
||||
name
|
||||
expiration))))
|
||||
[cfg profile-id name expiration]
|
||||
(db/tx-run! cfg create-access-token profile-id name expiration))
|
||||
|
||||
(def ^:private schema:create-access-token
|
||||
[:map {:title "create-access-token"}
|
||||
|
||||
@@ -149,7 +149,7 @@
|
||||
;; ---- COMMAND: Recover Profile
|
||||
|
||||
(defn recover-profile
|
||||
[{:keys [::db/pool] :as cfg} {:keys [token password]}]
|
||||
[{:keys [::db/conn] :as cfg} {:keys [token password]}]
|
||||
(letfn [(validate-token [token]
|
||||
(let [tdata (tokens/verify (::setup/props cfg) {:token token :iss :password-recovery})]
|
||||
(:profile-id tdata)))
|
||||
@@ -159,10 +159,10 @@
|
||||
(db/update! conn :profile {:password pwd :is-active true} {:id profile-id})
|
||||
nil))]
|
||||
|
||||
(db/with-atomic [conn pool]
|
||||
(->> (validate-token token)
|
||||
(update-password conn))
|
||||
nil)))
|
||||
(->> (validate-token token)
|
||||
(update-password conn))
|
||||
|
||||
nil))
|
||||
|
||||
(def schema:recover-profile
|
||||
[:map {:title "recover-profile"}
|
||||
@@ -173,7 +173,8 @@
|
||||
{::rpc/auth false
|
||||
::doc/added "1.15"
|
||||
::sm/params schema:recover-profile
|
||||
::climit/id :auth/global}
|
||||
::climit/id :auth/global
|
||||
::db/transaction true}
|
||||
[cfg params]
|
||||
(recover-profile cfg params))
|
||||
|
||||
|
||||
@@ -797,3 +797,18 @@
|
||||
{:id id}
|
||||
{::db/return-keys false})
|
||||
nil))
|
||||
|
||||
(def ^:private
|
||||
schema:mark-all-threads-as-read
|
||||
[:map {:title "mark-all-threads-as-read"}
|
||||
[:threads [:vector ::sm/uuid]]])
|
||||
|
||||
(sv/defmethod ::mark-all-threads-as-read
|
||||
{::doc/added "1.15"
|
||||
::sm/params schema:mark-all-threads-as-read}
|
||||
[cfg {:keys [::rpc/profile-id threads] :as params}]
|
||||
(db/tx-run!
|
||||
cfg
|
||||
(fn [{:keys [::db/conn]}]
|
||||
(doseq [thread-id threads]
|
||||
(upsert-comment-thread-status! conn profile-id thread-id)))))
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
{::rpc/auth false
|
||||
::doc/added "1.15"
|
||||
::doc/changes ["1.15" "This method is migrated from mutations to commands."]}
|
||||
[{:keys [::db/pool] :as cfg} _]
|
||||
[cfg _]
|
||||
|
||||
(when-not (contains? cf/flags :demo-users)
|
||||
(ex/raise :type :validation
|
||||
@@ -49,9 +49,11 @@
|
||||
:password (profile/derive-password cfg password)
|
||||
:props {}}]
|
||||
|
||||
(db/with-atomic [conn pool]
|
||||
(let [profile (->> (auth/create-profile! conn params)
|
||||
(auth/create-profile-rels! conn))]
|
||||
(with-meta {:email email
|
||||
:password password}
|
||||
{::audit/profile-id (:id profile)})))))
|
||||
|
||||
(let [profile (db/tx-run! cfg (fn [{:keys [::db/conn]}]
|
||||
(->> (auth/create-profile! conn params)
|
||||
(auth/create-profile-rels! conn))))]
|
||||
(with-meta {:email email
|
||||
:password password}
|
||||
{::audit/profile-id (:id profile)}))))
|
||||
|
||||
|
||||
@@ -323,6 +323,7 @@
|
||||
|
||||
file (-> (get-file cfg id :project-id project-id)
|
||||
(assoc :permissions perms)
|
||||
(assoc :team-id (:id team))
|
||||
(check-version!))]
|
||||
|
||||
(-> (cfeat/get-team-enabled-features cf/flags team)
|
||||
@@ -613,6 +614,7 @@
|
||||
SELECT l.id,
|
||||
l.features,
|
||||
l.project_id,
|
||||
p.team_id,
|
||||
l.created_at,
|
||||
l.modified_at,
|
||||
l.deleted_at,
|
||||
@@ -622,6 +624,7 @@
|
||||
l.synced_at,
|
||||
l.is_shared
|
||||
FROM libs AS l
|
||||
INNER JOIN project AS p ON (p.id = l.project_id)
|
||||
WHERE l.deleted_at IS NULL OR l.deleted_at > now();")
|
||||
|
||||
(defn get-file-libraries
|
||||
@@ -803,17 +806,17 @@
|
||||
[:id ::sm/uuid]
|
||||
[:name [:string {:max 250}]]
|
||||
[:created-at ::dt/instant]
|
||||
[:modified-at ::dt/instant]]}
|
||||
[:modified-at ::dt/instant]]
|
||||
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(check-edition-permissions! conn profile-id id)
|
||||
(let [file (rename-file conn params)]
|
||||
(rph/with-meta
|
||||
(select-keys file [:id :name :created-at :modified-at])
|
||||
{::audit/props {:project-id (:project-id file)
|
||||
:created-at (:created-at file)
|
||||
:modified-at (:modified-at file)}}))))
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id id] :as params}]
|
||||
(check-edition-permissions! conn profile-id id)
|
||||
(let [file (rename-file conn params)]
|
||||
(rph/with-meta
|
||||
(select-keys file [:id :name :created-at :modified-at])
|
||||
{::audit/props {:project-id (:project-id file)
|
||||
:created-at (:created-at file)
|
||||
:modified-at (:modified-at file)}})))
|
||||
|
||||
;; --- MUTATION COMMAND: set-file-shared
|
||||
|
||||
@@ -1005,15 +1008,17 @@
|
||||
{::doc/added "1.17"
|
||||
::webhooks/event? true
|
||||
::sm/params schema:link-file-to-library}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id library-id] :as params}]
|
||||
[cfg {:keys [::rpc/profile-id file-id library-id] :as params}]
|
||||
(when (= file-id library-id)
|
||||
(ex/raise :type :validation
|
||||
:code :invalid-library
|
||||
:hint "A file cannot be linked to itself"))
|
||||
(db/with-atomic [conn pool]
|
||||
(check-edition-permissions! conn profile-id file-id)
|
||||
(check-edition-permissions! conn profile-id library-id)
|
||||
(link-file-to-library conn params)))
|
||||
|
||||
(db/tx-run! cfg
|
||||
(fn [{:keys [::db/conn]}]
|
||||
(check-edition-permissions! conn profile-id file-id)
|
||||
(check-edition-permissions! conn profile-id library-id)
|
||||
(link-file-to-library conn params))))
|
||||
|
||||
;; --- MUTATION COMMAND: unlink-file-from-library
|
||||
|
||||
@@ -1031,12 +1036,12 @@
|
||||
(sv/defmethod ::unlink-file-from-library
|
||||
{::doc/added "1.17"
|
||||
::webhooks/event? true
|
||||
::sm/params schema:unlink-file-to-library}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(check-edition-permissions! conn profile-id file-id)
|
||||
(unlink-file-from-library conn params)
|
||||
nil))
|
||||
::sm/params schema:unlink-file-to-library
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
||||
(check-edition-permissions! conn profile-id file-id)
|
||||
(unlink-file-from-library conn params)
|
||||
nil)
|
||||
|
||||
;; --- MUTATION COMMAND: update-sync
|
||||
|
||||
@@ -1056,12 +1061,11 @@
|
||||
(sv/defmethod ::update-file-library-sync-status
|
||||
"Update the synchronization status of a file->library link"
|
||||
{::doc/added "1.17"
|
||||
::sm/params schema:update-file-library-sync-status}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(check-edition-permissions! conn profile-id file-id)
|
||||
(update-sync conn params)))
|
||||
|
||||
::sm/params schema:update-file-library-sync-status
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn]} {:keys [::rpc/profile-id file-id] :as params}]
|
||||
(check-edition-permissions! conn profile-id file-id)
|
||||
(update-sync conn params))
|
||||
|
||||
;; --- MUTATION COMMAND: ignore-sync
|
||||
|
||||
@@ -1082,9 +1086,9 @@
|
||||
(sv/defmethod ::ignore-file-library-sync-status
|
||||
"Ignore updates in linked files"
|
||||
{::doc/added "1.17"
|
||||
::sm/params schema:ignore-file-library-sync-status}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(check-edition-permissions! conn profile-id file-id)
|
||||
(-> (ignore-sync conn params)
|
||||
(update :features db/decode-pgarray #{}))))
|
||||
::sm/params schema:ignore-file-library-sync-status
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn]} {:keys [::rpc/profile-id file-id] :as params}]
|
||||
(check-edition-permissions! conn profile-id file-id)
|
||||
(-> (ignore-sync conn params)
|
||||
(update :features db/decode-pgarray #{})))
|
||||
|
||||
@@ -33,11 +33,11 @@
|
||||
pages of a file with specific permissions (who-comment and who-inspect)."
|
||||
{::doc/added "1.18"
|
||||
::doc/module :files
|
||||
::sm/params schema:create-share-link}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(files/check-edition-permissions! conn profile-id file-id)
|
||||
(create-share-link conn (assoc params :profile-id profile-id))))
|
||||
::sm/params schema:create-share-link
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn]} {:keys [::rpc/profile-id file-id] :as params}]
|
||||
(files/check-edition-permissions! conn profile-id file-id)
|
||||
(create-share-link conn (assoc params :profile-id profile-id)))
|
||||
|
||||
(defn create-share-link
|
||||
[conn {:keys [profile-id file-id pages who-comment who-inspect]}]
|
||||
@@ -61,10 +61,10 @@
|
||||
(sv/defmethod ::delete-share-link
|
||||
{::doc/added "1.18"
|
||||
::doc/module ::files
|
||||
::sm/params schema:delete-share-link}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [slink (db/get-by-id conn :share-link id)]
|
||||
(files/check-edition-permissions! conn profile-id (:file-id slink))
|
||||
(db/delete! conn :share-link {:id id})
|
||||
nil)))
|
||||
::sm/params schema:delete-share-link
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn]} {:keys [::rpc/profile-id id] :as params}]
|
||||
(let [slink (db/get-by-id conn :share-link id)]
|
||||
(files/check-edition-permissions! conn profile-id (:file-id slink))
|
||||
(db/delete! conn :share-link {:id id})
|
||||
nil))
|
||||
|
||||
@@ -396,45 +396,49 @@
|
||||
;; --- COMMAND: Clone Template
|
||||
|
||||
(defn clone-template
|
||||
[cfg {:keys [project-id profile-id] :as params} template]
|
||||
(db/tx-run! cfg (fn [{:keys [::db/conn ::wrk/executor] :as cfg}]
|
||||
;; NOTE: the importation process performs some operations
|
||||
;; that are not very friendly with virtual threads, and for
|
||||
;; avoid unexpected blocking of other concurrent operations
|
||||
;; we dispatch that operation to a dedicated executor.
|
||||
(let [template (tmp/tempfile-from template
|
||||
:prefix "penpot.template."
|
||||
:suffix ""
|
||||
:min-age "30m")
|
||||
[{:keys [::db/pool ::wrk/executor] :as cfg} {:keys [project-id profile-id] :as params} template]
|
||||
|
||||
format (bfc/parse-file-format template)
|
||||
team (teams/get-team conn
|
||||
:profile-id profile-id
|
||||
:project-id project-id)
|
||||
cfg (-> cfg
|
||||
(assoc ::bfc/project-id project-id)
|
||||
(assoc ::bfc/profile-id profile-id)
|
||||
(assoc ::bfc/input template)
|
||||
(assoc ::bfc/features (cfeat/get-team-enabled-features cf/flags team)))
|
||||
;; NOTE: the importation process performs some operations
|
||||
;; that are not very friendly with virtual threads, and for
|
||||
;; avoid unexpected blocking of other concurrent operations
|
||||
;; we dispatch that operation to a dedicated executor.
|
||||
(let [template (tmp/tempfile-from template
|
||||
:prefix "penpot.template."
|
||||
:suffix ""
|
||||
:min-age "30m")
|
||||
|
||||
result (if (= format :binfile-v3)
|
||||
(px/invoke! executor (partial bf.v3/import-files! cfg))
|
||||
(px/invoke! executor (partial bf.v1/import-files! cfg)))]
|
||||
format (bfc/parse-file-format template)
|
||||
team (teams/get-team pool
|
||||
:profile-id profile-id
|
||||
:project-id project-id)
|
||||
|
||||
(db/update! conn :project
|
||||
{:modified-at (dt/now)}
|
||||
{:id project-id})
|
||||
cfg (-> cfg
|
||||
(assoc ::bfc/project-id project-id)
|
||||
(assoc ::bfc/profile-id profile-id)
|
||||
(assoc ::bfc/input template)
|
||||
(assoc ::bfc/features (cfeat/get-team-enabled-features cf/flags team)))
|
||||
|
||||
(let [props (audit/clean-props params)]
|
||||
(doseq [file-id result]
|
||||
(let [props (assoc props :id file-id)
|
||||
event (-> (audit/event-from-rpc-params params)
|
||||
(assoc ::audit/profile-id profile-id)
|
||||
(assoc ::audit/name "create-file")
|
||||
(assoc ::audit/props props))]
|
||||
(audit/submit! cfg event))))
|
||||
result (if (= format :binfile-v3)
|
||||
(px/invoke! executor (partial bf.v3/import-files! cfg))
|
||||
(px/invoke! executor (partial bf.v1/import-files! cfg)))]
|
||||
|
||||
result))))
|
||||
(db/tx-run! cfg
|
||||
(fn [{:keys [::db/conn] :as cfg}]
|
||||
(db/update! conn :project
|
||||
{:modified-at (dt/now)}
|
||||
{:id project-id}
|
||||
{::db/return-keys false})
|
||||
|
||||
(let [props (audit/clean-props params)]
|
||||
(doseq [file-id result]
|
||||
(let [props (assoc props :id file-id)
|
||||
event (-> (audit/event-from-rpc-params params)
|
||||
(assoc ::audit/profile-id profile-id)
|
||||
(assoc ::audit/name "create-file")
|
||||
(assoc ::audit/props props))]
|
||||
(audit/submit! cfg event))))))
|
||||
|
||||
result))
|
||||
|
||||
(def ^:private
|
||||
schema:clone-template
|
||||
|
||||
@@ -273,15 +273,14 @@
|
||||
|
||||
(sv/defmethod ::clone-file-media-object
|
||||
{::doc/added "1.17"
|
||||
::sm/params schema:clone-file-media-object}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(files/check-edition-permissions! conn profile-id file-id)
|
||||
(-> (assoc cfg :conn conn)
|
||||
(clone-file-media-object params))))
|
||||
::sm/params schema:clone-file-media-object
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
||||
(files/check-edition-permissions! conn profile-id file-id)
|
||||
(clone-file-media-object cfg params))
|
||||
|
||||
(defn clone-file-media-object
|
||||
[{:keys [conn]} {:keys [id file-id is-local]}]
|
||||
[{:keys [::db/conn]} {:keys [id file-id is-local]}]
|
||||
(let [mobj (db/get-by-id conn :file-media-object id)]
|
||||
(db/insert! conn :file-media-object
|
||||
{:id (uuid/next)
|
||||
|
||||
@@ -125,32 +125,32 @@
|
||||
(sv/defmethod ::update-profile
|
||||
{::doc/added "1.0"
|
||||
::sm/params schema:update-profile
|
||||
::sm/result schema:profile}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id fullname lang theme] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
;; NOTE: we need to retrieve the profile independently if we use
|
||||
;; it or not for explicit locking and avoid concurrent updates of
|
||||
;; the same row/object.
|
||||
(let [profile (-> (db/get-by-id conn :profile profile-id ::sql/for-update true)
|
||||
(decode-row))
|
||||
::sm/result schema:profile
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn]} {:keys [::rpc/profile-id fullname lang theme] :as params}]
|
||||
;; NOTE: we need to retrieve the profile independently if we use
|
||||
;; it or not for explicit locking and avoid concurrent updates of
|
||||
;; the same row/object.
|
||||
(let [profile (-> (db/get-by-id conn :profile profile-id ::sql/for-update true)
|
||||
(decode-row))
|
||||
|
||||
;; Update the profile map with direct params
|
||||
profile (-> profile
|
||||
(assoc :fullname fullname)
|
||||
(assoc :lang lang)
|
||||
(assoc :theme theme))]
|
||||
;; Update the profile map with direct params
|
||||
profile (-> profile
|
||||
(assoc :fullname fullname)
|
||||
(assoc :lang lang)
|
||||
(assoc :theme theme))]
|
||||
|
||||
(db/update! conn :profile
|
||||
{:fullname fullname
|
||||
:lang lang
|
||||
:theme theme
|
||||
:props (db/tjson (:props profile))}
|
||||
{:id profile-id})
|
||||
(db/update! conn :profile
|
||||
{:fullname fullname
|
||||
:lang lang
|
||||
:theme theme
|
||||
:props (db/tjson (:props profile))}
|
||||
{:id profile-id})
|
||||
|
||||
(-> profile
|
||||
(strip-private-attrs)
|
||||
(d/without-nils)
|
||||
(rph/with-meta {::audit/props (audit/profile->props profile)})))))
|
||||
(-> profile
|
||||
(strip-private-attrs)
|
||||
(d/without-nils)
|
||||
(rph/with-meta {::audit/props (audit/profile->props profile)}))))
|
||||
|
||||
|
||||
;; --- MUTATION: Update Password
|
||||
@@ -169,21 +169,20 @@
|
||||
(sv/defmethod ::update-profile-password
|
||||
{::doc/added "1.0"
|
||||
::sm/params schema:update-profile-password
|
||||
::climit/id :auth/global}
|
||||
::climit/id :auth/global
|
||||
::db/transaction true}
|
||||
[cfg {:keys [::rpc/profile-id password] :as params}]
|
||||
(let [profile (validate-password! cfg (assoc params :profile-id profile-id))
|
||||
session-id (::session/id params)]
|
||||
|
||||
(db/tx-run! cfg (fn [cfg]
|
||||
(let [profile (validate-password! cfg (assoc params :profile-id profile-id))
|
||||
session-id (::session/id params)]
|
||||
(when (= (:email profile) (str/lower (:password params)))
|
||||
(ex/raise :type :validation
|
||||
:code :email-as-password
|
||||
:hint "you can't use your email as password"))
|
||||
|
||||
(when (= (:email profile) (str/lower (:password params)))
|
||||
(ex/raise :type :validation
|
||||
:code :email-as-password
|
||||
:hint "you can't use your email as password"))
|
||||
|
||||
(update-profile-password! cfg (assoc profile :password password))
|
||||
(invalidate-profile-session! cfg profile-id session-id)
|
||||
nil))))
|
||||
(update-profile-password! cfg (assoc profile :password password))
|
||||
(invalidate-profile-session! cfg profile-id session-id)
|
||||
nil))
|
||||
|
||||
(defn- invalidate-profile-session!
|
||||
"Removes all sessions except the current one."
|
||||
@@ -441,37 +440,36 @@
|
||||
(declare ^:private get-owned-teams)
|
||||
|
||||
(sv/defmethod ::delete-profile
|
||||
{::doc/added "1.0"}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [teams (get-owned-teams conn profile-id)
|
||||
deleted-at (dt/now)]
|
||||
{::doc/added "1.0"
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id] :as params}]
|
||||
(let [teams (get-owned-teams conn profile-id)
|
||||
deleted-at (dt/now)]
|
||||
|
||||
;; If we found owned teams with participants, we don't allow
|
||||
;; delete profile until the user properly transfer ownership or
|
||||
;; explicitly removes all participants from the team
|
||||
(when (some pos? (map :participants teams))
|
||||
(ex/raise :type :validation
|
||||
:code :owner-teams-with-people
|
||||
:hint "The user need to transfer ownership of owned teams."
|
||||
:context {:teams (mapv :id teams)}))
|
||||
;; If we found owned teams with participants, we don't allow
|
||||
;; delete profile until the user properly transfer ownership or
|
||||
;; explicitly removes all participants from the team
|
||||
(when (some pos? (map :participants teams))
|
||||
(ex/raise :type :validation
|
||||
:code :owner-teams-with-people
|
||||
:hint "The user need to transfer ownership of owned teams."
|
||||
:context {:teams (mapv :id teams)}))
|
||||
|
||||
;; Mark profile deleted immediatelly
|
||||
(db/update! conn :profile
|
||||
{:deleted-at deleted-at}
|
||||
{:id profile-id})
|
||||
;; Mark profile deleted immediatelly
|
||||
(db/update! conn :profile
|
||||
{:deleted-at deleted-at}
|
||||
{:id profile-id})
|
||||
|
||||
;; Schedule cascade deletion to a worker
|
||||
(wrk/submit! {::db/conn conn
|
||||
::wrk/task :delete-object
|
||||
::wrk/params {:object :profile
|
||||
:deleted-at deleted-at
|
||||
:id profile-id}})
|
||||
;; Schedule cascade deletion to a worker
|
||||
(wrk/submit! {::db/conn conn
|
||||
::wrk/task :delete-object
|
||||
::wrk/params {:object :profile
|
||||
:deleted-at deleted-at
|
||||
:id profile-id}})
|
||||
|
||||
|
||||
(-> (rph/wrap nil)
|
||||
(rph/with-transform (session/delete-fn cfg))))))
|
||||
|
||||
(-> (rph/wrap nil)
|
||||
(rph/with-transform (session/delete-fn cfg)))))
|
||||
|
||||
;; --- HELPERS
|
||||
|
||||
|
||||
@@ -219,12 +219,12 @@
|
||||
::sm/params schema:update-project-pin
|
||||
::webhooks/batch-timeout (dt/duration "5s")
|
||||
::webhooks/batch-key (webhooks/key-fn ::rpc/profile-id :id)
|
||||
::webhooks/event? true}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id team-id is-pinned] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(check-read-permissions! conn profile-id id)
|
||||
(db/exec-one! conn [sql:update-project-pin team-id id profile-id is-pinned is-pinned])
|
||||
nil))
|
||||
::webhooks/event? true
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn]} {:keys [::rpc/profile-id id team-id is-pinned] :as params}]
|
||||
(check-read-permissions! conn profile-id id)
|
||||
(db/exec-one! conn [sql:update-project-pin team-id id profile-id is-pinned is-pinned])
|
||||
nil)
|
||||
|
||||
;; --- MUTATION: Rename Project
|
||||
|
||||
@@ -238,17 +238,17 @@
|
||||
(sv/defmethod ::rename-project
|
||||
{::doc/added "1.18"
|
||||
::sm/params schema:rename-project
|
||||
::webhooks/event? true}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id name] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(check-edition-permissions! conn profile-id id)
|
||||
(let [project (db/get-by-id conn :project id ::sql/for-update true)]
|
||||
(db/update! conn :project
|
||||
{:name name}
|
||||
{:id id})
|
||||
(rph/with-meta (rph/wrap)
|
||||
{::audit/props {:team-id (:team-id project)
|
||||
:prev-name (:name project)}}))))
|
||||
::webhooks/event? true
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn]} {:keys [::rpc/profile-id id name] :as params}]
|
||||
(check-edition-permissions! conn profile-id id)
|
||||
(let [project (db/get-by-id conn :project id ::sql/for-update true)]
|
||||
(db/update! conn :project
|
||||
{:name name}
|
||||
{:id id})
|
||||
(rph/with-meta (rph/wrap)
|
||||
{::audit/props {:team-id (:team-id project)
|
||||
:prev-name (:name project)}})))
|
||||
|
||||
;; --- MUTATION: Delete Project
|
||||
|
||||
@@ -280,13 +280,13 @@
|
||||
(sv/defmethod ::delete-project
|
||||
{::doc/added "1.18"
|
||||
::sm/params schema:delete-project
|
||||
::webhooks/event? true}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(check-edition-permissions! conn profile-id id)
|
||||
(let [project (delete-project conn id)]
|
||||
(rph/with-meta (rph/wrap)
|
||||
{::audit/props {:team-id (:team-id project)
|
||||
:name (:name project)
|
||||
:created-at (:created-at project)
|
||||
:modified-at (:modified-at project)}}))))
|
||||
::webhooks/event? true
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn]} {:keys [::rpc/profile-id id] :as params}]
|
||||
(check-edition-permissions! conn profile-id id)
|
||||
(let [project (delete-project conn id)]
|
||||
(rph/with-meta (rph/wrap)
|
||||
{::audit/props {:team-id (:team-id project)
|
||||
:name (:name project)
|
||||
:created-at (:created-at project)
|
||||
:modified-at (:modified-at project)}})))
|
||||
|
||||
@@ -527,14 +527,14 @@
|
||||
|
||||
(sv/defmethod ::update-team
|
||||
{::doc/added "1.17"
|
||||
::sm/params schema:update-team}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id name] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(check-edition-permissions! conn profile-id id)
|
||||
(db/update! conn :team
|
||||
{:name name}
|
||||
{:id id})
|
||||
nil))
|
||||
::sm/params schema:update-team
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id id name]}]
|
||||
(check-edition-permissions! conn profile-id id)
|
||||
(db/update! conn :team
|
||||
{:name name}
|
||||
{:id id})
|
||||
nil)
|
||||
|
||||
|
||||
;; --- Mutation: Leave Team
|
||||
@@ -592,10 +592,10 @@
|
||||
|
||||
(sv/defmethod ::leave-team
|
||||
{::doc/added "1.17"
|
||||
::sm/params schema:leave-team}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(leave-team conn (assoc params :profile-id profile-id))))
|
||||
::sm/params schema:leave-team
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id] :as params}]
|
||||
(leave-team conn (assoc params :profile-id profile-id)))
|
||||
|
||||
;; --- Mutation: Delete Team
|
||||
|
||||
@@ -627,16 +627,16 @@
|
||||
|
||||
(sv/defmethod ::delete-team
|
||||
{::doc/added "1.17"
|
||||
::sm/params schema:delete-team}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [perms (get-permissions conn profile-id id)]
|
||||
(when-not (:is-owner perms)
|
||||
(ex/raise :type :validation
|
||||
:code :only-owner-can-delete-team))
|
||||
::sm/params schema:delete-team
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id id] :as params}]
|
||||
(let [perms (get-permissions conn profile-id id)]
|
||||
(when-not (:is-owner perms)
|
||||
(ex/raise :type :validation
|
||||
:code :only-owner-can-delete-team))
|
||||
|
||||
(delete-team conn id)
|
||||
nil)))
|
||||
(delete-team conn id)
|
||||
nil))
|
||||
|
||||
;; --- Mutation: Team Update Role
|
||||
|
||||
@@ -714,31 +714,30 @@
|
||||
|
||||
(sv/defmethod ::delete-team-member
|
||||
{::doc/added "1.17"
|
||||
::sm/params schema:delete-team-member}
|
||||
[{:keys [::db/pool ::mbus/msgbus] :as cfg} {:keys [::rpc/profile-id team-id member-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [team (get-team pool :profile-id profile-id :team-id team-id)
|
||||
perms (get-permissions conn profile-id team-id)]
|
||||
(when-not (or (:is-owner perms)
|
||||
(:is-admin perms))
|
||||
(ex/raise :type :validation
|
||||
:code :insufficient-permissions))
|
||||
::sm/params schema:delete-team-member
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn ::mbus/msgbus] :as cfg} {:keys [::rpc/profile-id team-id member-id] :as params}]
|
||||
(let [team (get-team conn :profile-id profile-id :team-id team-id)
|
||||
perms (get-permissions conn profile-id team-id)]
|
||||
(when-not (or (:is-owner perms)
|
||||
(:is-admin perms))
|
||||
(ex/raise :type :validation
|
||||
:code :insufficient-permissions))
|
||||
|
||||
(when (= member-id profile-id)
|
||||
(ex/raise :type :validation
|
||||
:code :cant-remove-yourself))
|
||||
(when (= member-id profile-id)
|
||||
(ex/raise :type :validation
|
||||
:code :cant-remove-yourself))
|
||||
|
||||
(db/delete! conn :team-profile-rel {:profile-id member-id
|
||||
:team-id team-id})
|
||||
(db/delete! conn :team-profile-rel {:profile-id member-id
|
||||
:team-id team-id})
|
||||
(mbus/pub! msgbus
|
||||
:topic member-id
|
||||
:message {:type :team-membership-change
|
||||
:change :removed
|
||||
:team-id team-id
|
||||
:team-name (:name team)})
|
||||
|
||||
(mbus/pub! msgbus
|
||||
:topic member-id
|
||||
:message {:type :team-membership-change
|
||||
:change :removed
|
||||
:team-id team-id
|
||||
:team-name (:name team)})
|
||||
|
||||
nil)))
|
||||
nil))
|
||||
|
||||
;; --- Mutation: Update Team Photo
|
||||
|
||||
@@ -764,16 +763,16 @@
|
||||
(let [team (get-team pool :profile-id profile-id :team-id team-id)
|
||||
photo (profile/upload-photo cfg params)]
|
||||
|
||||
(db/with-atomic [conn pool]
|
||||
(check-admin-permissions! conn profile-id team-id)
|
||||
;; Mark object as touched for make it ellegible for tentative
|
||||
;; garbage collection.
|
||||
(when-let [id (:photo-id team)]
|
||||
(sto/touch-object! storage id))
|
||||
(check-admin-permissions! pool profile-id team-id)
|
||||
|
||||
;; Save new photo
|
||||
(db/update! pool :team
|
||||
{:photo-id (:id photo)}
|
||||
{:id team-id})
|
||||
;; Mark object as touched for make it ellegible for tentative
|
||||
;; garbage collection.
|
||||
(when-let [id (:photo-id team)]
|
||||
(sto/touch-object! storage id))
|
||||
|
||||
(assoc team :photo-id (:id photo)))))
|
||||
;; Save new photo
|
||||
(db/update! pool :team
|
||||
{:photo-id (:id photo)}
|
||||
{:id team-id})
|
||||
|
||||
(assoc team :photo-id (:id photo))))
|
||||
|
||||
@@ -408,20 +408,20 @@
|
||||
(sv/defmethod ::update-team-invitation-role
|
||||
{::doc/added "1.17"
|
||||
::doc/module :teams
|
||||
::sm/params schema:update-team-invitation-role}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id email role] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [perms (teams/get-permissions conn profile-id team-id)]
|
||||
::sm/params schema:update-team-invitation-role
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn]} {:keys [::rpc/profile-id team-id email role] :as params}]
|
||||
(let [perms (teams/get-permissions conn profile-id team-id)]
|
||||
|
||||
(when-not (:is-admin perms)
|
||||
(ex/raise :type :validation
|
||||
:code :insufficient-permissions))
|
||||
(when-not (:is-admin perms)
|
||||
(ex/raise :type :validation
|
||||
:code :insufficient-permissions))
|
||||
|
||||
(db/update! conn :team-invitation
|
||||
{:role (name role) :updated-at (dt/now)}
|
||||
{:team-id team-id :email-to (profile/clean-email email)})
|
||||
(db/update! conn :team-invitation
|
||||
{:role (name role) :updated-at (dt/now)}
|
||||
{:team-id team-id :email-to (profile/clean-email email)})
|
||||
|
||||
nil)))
|
||||
nil))
|
||||
|
||||
;; --- Mutation: Delete invitation
|
||||
|
||||
@@ -432,20 +432,20 @@
|
||||
|
||||
(sv/defmethod ::delete-team-invitation
|
||||
{::doc/added "1.17"
|
||||
::sm/params schema:delete-team-invition}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id email] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [perms (teams/get-permissions conn profile-id team-id)]
|
||||
::sm/params schema:delete-team-invition
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn]} {:keys [::rpc/profile-id team-id email] :as params}]
|
||||
(let [perms (teams/get-permissions conn profile-id team-id)]
|
||||
|
||||
(when-not (:is-admin perms)
|
||||
(ex/raise :type :validation
|
||||
:code :insufficient-permissions))
|
||||
(when-not (:is-admin perms)
|
||||
(ex/raise :type :validation
|
||||
:code :insufficient-permissions))
|
||||
|
||||
(let [invitation (db/delete! conn :team-invitation
|
||||
{:team-id team-id
|
||||
:email-to (profile/clean-email email)}
|
||||
{::db/return-keys true})]
|
||||
(rph/wrap nil {::audit/props {:invitation-id (:id invitation)}})))))
|
||||
(let [invitation (db/delete! conn :team-invitation
|
||||
{:team-id team-id
|
||||
:email-to (profile/clean-email email)}
|
||||
{::db/return-keys true})]
|
||||
(rph/wrap nil {::audit/props {:invitation-id (:id invitation)}}))))
|
||||
|
||||
|
||||
;; --- Mutation: Request Team Invitation
|
||||
|
||||
@@ -144,20 +144,20 @@
|
||||
|
||||
(sv/defmethod ::delete-webhook
|
||||
{::doc/added "1.17"
|
||||
::sm/params schema:delete-webhook}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id]}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [whook (-> (db/get conn :webhook {:id id}) decode-row)]
|
||||
(check-webhook-edition-permissions! conn profile-id (:team-id whook) (:profile-id whook))
|
||||
(db/delete! conn :webhook {:id id})
|
||||
nil)))
|
||||
::sm/params schema:delete-webhook
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn]} {:keys [::rpc/profile-id id]}]
|
||||
(let [whook (-> (db/get conn :webhook {:id id}) decode-row)]
|
||||
(check-webhook-edition-permissions! conn profile-id (:team-id whook) (:profile-id whook))
|
||||
(db/delete! conn :webhook {:id id})
|
||||
nil))
|
||||
|
||||
;; --- Query: Webhooks
|
||||
|
||||
(def sql:get-webhooks
|
||||
"SELECT id, uri, mtype, is_active, error_code, error_count, profile_id
|
||||
FROM webhook
|
||||
WHERE team_id = ?
|
||||
"SELECT id, uri, mtype, is_active, error_code, error_count, profile_id
|
||||
FROM webhook
|
||||
WHERE team_id = ?
|
||||
ORDER BY uri")
|
||||
|
||||
(def ^:private schema:get-webhooks
|
||||
|
||||
@@ -78,19 +78,19 @@
|
||||
|
||||
(defmethod ig/init-key ::props
|
||||
[_ {:keys [::db/pool ::key] :as cfg}]
|
||||
(db/with-atomic [conn pool]
|
||||
(db/xact-lock! conn 0)
|
||||
(when-not key
|
||||
(l/warn :hint (str "using autogenerated secret-key, it will change on each restart and will invalidate "
|
||||
"all sessions on each restart, it is highly recommended setting up the "
|
||||
"PENPOT_SECRET_KEY environment variable")))
|
||||
|
||||
(let [secret (or key (generate-random-key))]
|
||||
(-> (get-all-props conn)
|
||||
(assoc :secret-key secret)
|
||||
(assoc :tokens-key (keys/derive secret :salt "tokens"))
|
||||
(update :instance-id handle-instance-id conn (db/read-only? pool))))))
|
||||
(db/tx-run! cfg (fn [{:keys [::db/conn]}]
|
||||
(db/xact-lock! conn 0)
|
||||
(when-not key
|
||||
(l/warn :hint (str "using autogenerated secret-key, it will change on each restart and will invalidate "
|
||||
"all sessions on each restart, it is highly recommended setting up the "
|
||||
"PENPOT_SECRET_KEY environment variable")))
|
||||
|
||||
(let [secret (or key (generate-random-key))]
|
||||
(-> (get-all-props conn)
|
||||
(assoc :secret-key secret)
|
||||
(assoc :tokens-key (keys/derive secret :salt "tokens"))
|
||||
(update :instance-id handle-instance-id conn (db/read-only? pool)))))))
|
||||
|
||||
;; FIXME
|
||||
(sm/register! ::props :any)
|
||||
|
||||
@@ -36,37 +36,39 @@
|
||||
(defmethod exec-command :create-profile
|
||||
[{:keys [fullname email password is-active]
|
||||
:or {is-active true}}]
|
||||
(when-let [system (get-current-system)]
|
||||
(db/with-atomic [conn (:app.db/pool system)]
|
||||
(let [password (cmd.profile/derive-password system password)
|
||||
params {:id (uuid/next)
|
||||
:email email
|
||||
:fullname fullname
|
||||
:is-active is-active
|
||||
:password password
|
||||
:props {}}]
|
||||
(->> (cmd.auth/create-profile! conn params)
|
||||
(cmd.auth/create-profile-rels! conn))))))
|
||||
(some-> (get-current-system)
|
||||
(db/tx-run!
|
||||
(fn [{:keys [::db/conn] :as system}]
|
||||
(let [password (cmd.profile/derive-password system password)
|
||||
params {:id (uuid/next)
|
||||
:email email
|
||||
:fullname fullname
|
||||
:is-active is-active
|
||||
:password password
|
||||
:props {}}]
|
||||
(->> (cmd.auth/create-profile! conn params)
|
||||
(cmd.auth/create-profile-rels! conn)))))))
|
||||
|
||||
(defmethod exec-command :update-profile
|
||||
[{:keys [fullname email password is-active]}]
|
||||
(when-let [system (get-current-system)]
|
||||
(db/with-atomic [conn (:app.db/pool system)]
|
||||
(let [params (cond-> {}
|
||||
(some? fullname)
|
||||
(assoc :fullname fullname)
|
||||
(some-> (get-current-system)
|
||||
(db/tx-run!
|
||||
(fn [{:keys [::db/conn] :as system}]
|
||||
(let [params (cond-> {}
|
||||
(some? fullname)
|
||||
(assoc :fullname fullname)
|
||||
|
||||
(some? password)
|
||||
(assoc :password (auth/derive-password password))
|
||||
(some? password)
|
||||
(assoc :password (auth/derive-password password))
|
||||
|
||||
(some? is-active)
|
||||
(assoc :is-active is-active))]
|
||||
(when (seq params)
|
||||
(let [res (db/update! conn :profile
|
||||
params
|
||||
{:email email
|
||||
:deleted-at nil})]
|
||||
(pos? (db/get-update-count res))))))))
|
||||
(some? is-active)
|
||||
(assoc :is-active is-active))]
|
||||
(when (seq params)
|
||||
(let [res (db/update! conn :profile
|
||||
params
|
||||
{:email email
|
||||
:deleted-at nil})]
|
||||
(pos? (db/get-update-count res)))))))))
|
||||
|
||||
(defmethod exec-command :delete-profile
|
||||
[{:keys [email soft]}]
|
||||
@@ -75,16 +77,16 @@
|
||||
:code :invalid-arguments
|
||||
:hint "email should be provided"))
|
||||
|
||||
(when-let [system (get-current-system)]
|
||||
(db/with-atomic [conn (:app.db/pool system)]
|
||||
|
||||
(let [res (if soft
|
||||
(db/update! conn :profile
|
||||
{:deleted-at (dt/now)}
|
||||
{:email email :deleted-at nil})
|
||||
(db/delete! conn :profile
|
||||
{:email email}))]
|
||||
(pos? (db/get-update-count res))))))
|
||||
(some-> (get-current-system)
|
||||
(db/tx-run!
|
||||
(fn [{:keys [::db/conn] :as system}]
|
||||
(let [res (if soft
|
||||
(db/update! conn :profile
|
||||
{:deleted-at (dt/now)}
|
||||
{:email email :deleted-at nil})
|
||||
(db/delete! conn :profile
|
||||
{:email email}))]
|
||||
(pos? (db/get-update-count res)))))))
|
||||
|
||||
(defmethod exec-command :search-profile
|
||||
[{:keys [email]}]
|
||||
@@ -93,12 +95,12 @@
|
||||
:code :invalid-arguments
|
||||
:hint "email should be provided"))
|
||||
|
||||
(when-let [system (get-current-system)]
|
||||
(db/with-atomic [conn (:app.db/pool system)]
|
||||
|
||||
(let [sql (str "select email, fullname, created_at, deleted_at from profile "
|
||||
" where email similar to ? order by created_at desc limit 100")]
|
||||
(db/exec! conn [sql email])))))
|
||||
(some-> (get-current-system)
|
||||
(db/tx-run!
|
||||
(fn [{:keys [::db/conn] :as system}]
|
||||
(let [sql (str "select email, fullname, created_at, deleted_at from profile "
|
||||
" where email similar to ? order by created_at desc limit 100")]
|
||||
(db/exec! conn [sql email]))))))
|
||||
|
||||
(defmethod exec-command :derive-password
|
||||
[{:keys [password]}]
|
||||
|
||||
@@ -101,38 +101,46 @@
|
||||
"Mark the profile blocked and removes all the http sessiones
|
||||
associated with the profile-id."
|
||||
[email]
|
||||
(db/with-atomic [conn (:app.db/pool main/system)]
|
||||
(when-let [profile (db/get* conn :profile
|
||||
{:email (str/lower email)}
|
||||
{:columns [:id :email]})]
|
||||
(when-not (:is-blocked profile)
|
||||
(db/update! conn :profile {:is-active true} {:id (:id profile)})
|
||||
:activated))))
|
||||
(some-> main/system
|
||||
(db/tx-run!
|
||||
(fn [{:keys [::db/conn] :as system}]
|
||||
(when-let [profile (db/get* conn :profile
|
||||
{:email (str/lower email)}
|
||||
{:columns [:id :email]})]
|
||||
(when-not (:is-blocked profile)
|
||||
(db/update! conn :profile {:is-active true} {:id (:id profile)})
|
||||
:activated))))))
|
||||
|
||||
(defn mark-profile-as-blocked!
|
||||
"Mark the profile blocked and removes all the http sessiones
|
||||
associated with the profile-id."
|
||||
[email]
|
||||
(db/with-atomic [conn (:app.db/pool main/system)]
|
||||
(when-let [profile (db/get* conn :profile
|
||||
{:email (str/lower email)}
|
||||
{:columns [:id :email]})]
|
||||
(when-not (:is-blocked profile)
|
||||
(db/update! conn :profile {:is-blocked true} {:id (:id profile)})
|
||||
(db/delete! conn :http-session {:profile-id (:id profile)})
|
||||
:blocked))))
|
||||
(some-> main/system
|
||||
(db/tx-run!
|
||||
(fn [{:keys [::db/conn] :as system}]
|
||||
(when-let [profile (db/get* conn :profile
|
||||
{:email (str/lower email)}
|
||||
{:columns [:id :email]})]
|
||||
(when-not (:is-blocked profile)
|
||||
(db/update! conn :profile {:is-blocked true} {:id (:id profile)})
|
||||
(db/delete! conn :http-session {:profile-id (:id profile)})
|
||||
:blocked))))))
|
||||
|
||||
(defn reset-password!
|
||||
"Reset a password to a specific one for a concrete user or all users
|
||||
if email is `:all` keyword."
|
||||
[& {:keys [email password] :or {password "123123"} :as params}]
|
||||
(us/verify! (contains? params :email) "`email` parameter is mandatory")
|
||||
(db/with-atomic [conn (:app.db/pool main/system)]
|
||||
(let [password (derive-password password)]
|
||||
(if (= email :all)
|
||||
(db/exec! conn ["update profile set password=?" password])
|
||||
(let [email (str/lower email)]
|
||||
(db/exec! conn ["update profile set password=? where email=?" password email]))))))
|
||||
(when-not email
|
||||
(throw (IllegalArgumentException. "email is mandatory")))
|
||||
|
||||
(some-> main/system
|
||||
(db/tx-run!
|
||||
(fn [{:keys [::db/conn] :as system}]
|
||||
(let [password (derive-password password)]
|
||||
(if (= email :all)
|
||||
(db/exec! conn ["update profile set password=?" password])
|
||||
(let [email (str/lower email)]
|
||||
(db/exec! conn ["update profile set password=? where email=?" password email]))))))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; FEATURES
|
||||
@@ -154,8 +162,8 @@
|
||||
(enable-pointer-map-feature-on-file! file-id opts))
|
||||
|
||||
(defn enable-team-feature!
|
||||
[team-id feature]
|
||||
(when-not (contains? cfeat/supported-features feature)
|
||||
[team-id feature & {:keys [skip-check] :or {skip-check false}}]
|
||||
(when (and (not skip-check) (not (contains? cfeat/supported-features feature)))
|
||||
(ex/raise :type :assertion
|
||||
:code :feature-not-supported
|
||||
:hint (str "feature '" feature "' not supported")))
|
||||
@@ -173,9 +181,8 @@
|
||||
:enabled))))))
|
||||
|
||||
(defn disable-team-feature!
|
||||
[team-id feature]
|
||||
|
||||
(when-not (contains? cfeat/supported-features feature)
|
||||
[team-id feature & {:keys [skip-check] :or {skip-check false}}]
|
||||
(when (and (not skip-check) (not (contains? cfeat/supported-features feature)))
|
||||
(ex/raise :type :assertion
|
||||
:code :feature-not-supported
|
||||
:hint (str "feature '" feature "' not supported")))
|
||||
|
||||
@@ -26,18 +26,14 @@
|
||||
{k (assoc v ::min-age (cf/get-deletion-delay))})
|
||||
|
||||
(defmethod ig/init-key ::handler
|
||||
[_ {:keys [::db/pool ::min-age] :as cfg}]
|
||||
[_ {:keys [::min-age] :as cfg}]
|
||||
(fn [{:keys [props] :as task}]
|
||||
(let [min-age (or (:min-age props) min-age)]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [interval (db/interval min-age)
|
||||
result (db/exec-one! conn [sql:delete-completed-tasks interval])
|
||||
result (db/get-update-count result)]
|
||||
|
||||
(l/debug :hint "task finished" :total result)
|
||||
|
||||
(when (:rollback? props)
|
||||
(db/rollback! conn))
|
||||
|
||||
result)))))
|
||||
|
||||
(-> cfg
|
||||
(assoc ::db/rollback (:rollback? props))
|
||||
(db/tx-run! (fn [{:keys [::db/conn]}]
|
||||
(let [interval (db/interval min-age)
|
||||
result (db/exec-one! conn [sql:delete-completed-tasks interval])
|
||||
result (db/get-update-count result)]
|
||||
(l/debug :hint "task finished" :total result)
|
||||
result)))))))
|
||||
|
||||
@@ -71,11 +71,12 @@
|
||||
|
||||
(run-batch! [rconn]
|
||||
(try
|
||||
(db/with-atomic [conn pool]
|
||||
(if-let [tasks (get-tasks conn)]
|
||||
(->> (group-by :queue tasks)
|
||||
(run! (partial push-tasks! conn rconn)))
|
||||
(px/sleep (::wait-duration cfg))))
|
||||
(db/tx-run! cfg (fn [{:keys [::db/conn]}]
|
||||
(if-let [tasks (get-tasks conn)]
|
||||
(->> (group-by :queue tasks)
|
||||
(run! (partial push-tasks! conn rconn)))
|
||||
;; FIXME: this sleep should be outside the transaction
|
||||
(px/sleep (::wait-duration cfg)))))
|
||||
(catch InterruptedException cause
|
||||
(throw cause))
|
||||
(catch Exception cause
|
||||
|
||||
@@ -138,14 +138,13 @@
|
||||
" FROM information_schema.tables "
|
||||
" WHERE table_schema = 'public' "
|
||||
" AND table_name != 'migrations';")]
|
||||
(db/with-atomic [conn *pool*]
|
||||
(db/exec-one! conn ["SET CONSTRAINTS ALL DEFERRED"])
|
||||
(db/exec-one! conn ["SET LOCAL rules.deletion_protection TO off"])
|
||||
(let [result (->> (db/exec! conn [sql])
|
||||
(map :table-name))]
|
||||
(doseq [table result]
|
||||
(db/exec! conn [(str "delete from " table ";")]))))
|
||||
|
||||
(db/transact! *pool* (fn [conn]
|
||||
(db/exec-one! conn ["SET CONSTRAINTS ALL DEFERRED"])
|
||||
(db/exec-one! conn ["SET LOCAL rules.deletion_protection TO off"])
|
||||
(let [result (->> (db/exec! conn [sql])
|
||||
(map :table-name))]
|
||||
(doseq [table result]
|
||||
(db/exec! conn [(str "delete from " table ";")])))))
|
||||
(next)))
|
||||
|
||||
(defn clean-storage
|
||||
|
||||
@@ -20,45 +20,33 @@
|
||||
(t/use-fixtures :each th/database-reset)
|
||||
|
||||
(t/deftest soft-auth-middleware
|
||||
(db/with-atomic [conn (::db/pool th/*system*)]
|
||||
(let [profile (th/create-profile* 1)
|
||||
system (-> th/*system*
|
||||
(assoc ::db/conn conn)
|
||||
(assoc ::main/props (:app.setup/props th/*system*)))
|
||||
(let [profile (th/create-profile* 1)
|
||||
token (db/tx-run! th/*system* app.rpc.commands.access-token/create-access-token (:id profile) "test" nil)
|
||||
|
||||
token (app.rpc.commands.access-token/create-access-token
|
||||
system (:id profile) "test" nil)
|
||||
request (volatile! nil)
|
||||
handler (#'app.http.access-token/wrap-soft-auth
|
||||
(fn [req] (vreset! request req))
|
||||
th/*system*)]
|
||||
|
||||
request (volatile! nil)
|
||||
handler (#'app.http.access-token/wrap-soft-auth
|
||||
(fn [req] (vreset! request req))
|
||||
system)]
|
||||
(with-mocks [m1 {:target 'app.http.access-token/get-token
|
||||
:return nil}]
|
||||
(handler {})
|
||||
(t/is (= {} @request)))
|
||||
|
||||
(with-mocks [m1 {:target 'app.http.access-token/get-token
|
||||
:return nil}]
|
||||
(handler {})
|
||||
(t/is (= {} @request)))
|
||||
(with-mocks [m1 {:target 'app.http.access-token/get-token
|
||||
:return (:token token)}]
|
||||
(handler {})
|
||||
|
||||
(with-mocks [m1 {:target 'app.http.access-token/get-token
|
||||
:return (:token token)}]
|
||||
(handler {})
|
||||
|
||||
(let [token-id (get @request :app.http.access-token/id)]
|
||||
(t/is (= token-id (:id token))))))))
|
||||
(let [token-id (get @request :app.http.access-token/id)]
|
||||
(t/is (= token-id (:id token)))))))
|
||||
|
||||
(t/deftest authz-middleware
|
||||
(let [profile (th/create-profile* 1)
|
||||
system (assoc th/*system* ::main/props (:app.setup/props th/*system*))
|
||||
|
||||
token (db/with-atomic [conn (::db/pool th/*system*)]
|
||||
(let [system (assoc system ::db/conn conn)]
|
||||
(app.rpc.commands.access-token/create-access-token
|
||||
system (:id profile) "test" nil)))
|
||||
|
||||
token (db/tx-run! th/*system* app.rpc.commands.access-token/create-access-token (:id profile) "test" nil)
|
||||
request (volatile! {})
|
||||
handler (#'app.http.access-token/wrap-authz
|
||||
(fn [req] (vreset! request req))
|
||||
system)]
|
||||
th/*system*)]
|
||||
|
||||
(handler nil)
|
||||
(t/is (nil? @request))
|
||||
|
||||
@@ -342,13 +342,20 @@
|
||||
(-> (hex->hsl data)
|
||||
(conj opacity)))
|
||||
|
||||
#?(:cljs
|
||||
(defn format-hsla
|
||||
[[h s l a]]
|
||||
(let [precision 2
|
||||
rounded-s (* 100 (parse-double (d/format-precision s precision)))
|
||||
rounded-l (* 100 (parse-double (d/format-precision l precision)))]
|
||||
(str/concat "" h ", " rounded-s "%, " rounded-l "%, " a))))
|
||||
(defn format-hsla
|
||||
[[h s l a]]
|
||||
(let [precision 2
|
||||
rounded-h (int h)
|
||||
rounded-s (d/format-number (* 100 s) precision)
|
||||
rounded-l (d/format-number (* 100 l) precision)
|
||||
rounded-a (d/format-number a precision)]
|
||||
(str/concat "" rounded-h ", " rounded-s "%, " rounded-l "%, " rounded-a)))
|
||||
|
||||
(defn format-rgba
|
||||
[[r g b a]]
|
||||
(let [precision 2
|
||||
rounded-a (d/format-number a precision)]
|
||||
(str/ffmt "%, %, %, %" r g b rounded-a)))
|
||||
|
||||
(defn- hue->rgb
|
||||
"Helper for hsl->rgb"
|
||||
|
||||
@@ -1007,27 +1007,35 @@
|
||||
(def ^:const trail-zeros-regex-1 #"\.0+$")
|
||||
(def ^:const trail-zeros-regex-2 #"(\.\d*[^0])0+$")
|
||||
|
||||
#?(:cljs
|
||||
(defn format-precision
|
||||
"Creates a number with predetermined precision and then removes the trailing 0.
|
||||
(defn format-precision
|
||||
"Creates a number with predetermined precision and then removes the trailing 0.
|
||||
Examples:
|
||||
12.0123, 0 => 12
|
||||
12.0123, 1 => 12
|
||||
12.0123, 2 => 12.01"
|
||||
[num precision]
|
||||
[num precision]
|
||||
|
||||
(if (number? num)
|
||||
(try
|
||||
(let [num-str (mth/to-fixed num precision)
|
||||
(if (number? num)
|
||||
(try
|
||||
(let [num-str (mth/to-fixed num precision)
|
||||
;; Remove all trailing zeros after the comma 100.00000
|
||||
num-str (str/replace num-str trail-zeros-regex-1 "")]
|
||||
num-str (str/replace num-str trail-zeros-regex-1 "")]
|
||||
;; Remove trailing zeros after a decimal number: 0.001|00|
|
||||
(if-let [m (re-find trail-zeros-regex-2 num-str)]
|
||||
(str/replace num-str (first m) (second m))
|
||||
num-str))
|
||||
(catch :default _
|
||||
(str num)))
|
||||
(str num))))
|
||||
(if-let [m (re-find trail-zeros-regex-2 num-str)]
|
||||
(str/replace num-str (first m) (second m))
|
||||
num-str))
|
||||
(catch #?(:clj Throwable :cljs :default) _
|
||||
(str num)))
|
||||
(str num)))
|
||||
|
||||
(defn format-number
|
||||
([value]
|
||||
(format-number value nil))
|
||||
([value {:keys [precision] :or {precision 2}}]
|
||||
(let [value (if (string? value) (parse-double value) value)]
|
||||
(when (num? value)
|
||||
(let [value (format-precision value precision)]
|
||||
(str value))))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Util protocols
|
||||
|
||||
@@ -52,7 +52,8 @@
|
||||
"plugins/runtime"
|
||||
"design-tokens/v1"
|
||||
"text-editor/v2"
|
||||
"render-wasm/v1"})
|
||||
"render-wasm/v1"
|
||||
"variants/v1"})
|
||||
|
||||
;; A set of features enabled by default
|
||||
(def default-features
|
||||
@@ -111,6 +112,7 @@
|
||||
:feature-design-tokens "design-tokens/v1"
|
||||
:feature-text-editor-v2 "text-editor/v2"
|
||||
:feature-render-wasm "render-wasm/v1"
|
||||
:feature-variants "variants/v1"
|
||||
nil))
|
||||
|
||||
(defn migrate-legacy-features
|
||||
|
||||
@@ -371,62 +371,17 @@
|
||||
[:type [:= :del-typography]]
|
||||
[:id ::sm/uuid]]]
|
||||
|
||||
[:add-temporary-token-theme
|
||||
[:map {:title "AddTemporaryTokenThemeChange"}
|
||||
[:type [:= :add-temporary-token-theme]]
|
||||
[:token-theme ::ctot/token-theme]]]
|
||||
|
||||
[:update-active-token-themes
|
||||
[:map {:title "UpdateActiveTokenThemes"}
|
||||
[:type [:= :update-active-token-themes]]
|
||||
[:theme-ids [:set :string]]]]
|
||||
|
||||
[:delete-temporary-token-theme
|
||||
[:map {:title "DeleteTemporaryTokenThemeChange"}
|
||||
[:type [:= :delete-temporary-token-theme]]
|
||||
[:id ::sm/uuid]
|
||||
[:name :string]]]
|
||||
|
||||
[:add-token-theme
|
||||
[:map {:title "AddTokenThemeChange"}
|
||||
[:type [:= :add-token-theme]]
|
||||
[:token-theme ::ctot/token-theme]]]
|
||||
|
||||
[:mod-token-theme
|
||||
[:map {:title "ModTokenThemeChange"}
|
||||
[:type [:= :mod-token-theme]]
|
||||
[:group :string]
|
||||
[:name :string]
|
||||
[:token-theme ::ctot/token-theme]]]
|
||||
|
||||
[:del-token-theme
|
||||
[:map {:title "DelTokenThemeChange"}
|
||||
[:type [:= :del-token-theme]]
|
||||
[:group :string]
|
||||
[:name :string]]]
|
||||
|
||||
[:add-token-set
|
||||
[:map {:title "AddTokenSetChange"}
|
||||
[:type [:= :add-token-set]]
|
||||
[:token-set ::ctot/token-set]]]
|
||||
|
||||
[:add-token-sets
|
||||
[:map {:title "AddTokenSetsChange"}
|
||||
[:type [:= :add-token-sets]]
|
||||
[:token-sets [:sequential ::ctot/token-set]]]]
|
||||
|
||||
[:rename-token-set-group
|
||||
[:map {:title "RenameTokenSetGroup"}
|
||||
[:type [:= :rename-token-set-group]]
|
||||
[:set-group-path [:vector :string]]
|
||||
[:set-group-fname :string]]]
|
||||
|
||||
[:mod-token-set
|
||||
[:map {:title "ModTokenSetChange"}
|
||||
[:type [:= :mod-token-set]]
|
||||
[:name :string]
|
||||
[:token-set ::ctot/token-set]]]
|
||||
|
||||
[:move-token-set-before
|
||||
[:map {:title "MoveTokenSetBefore"}
|
||||
[:type [:= :move-token-set-before]]
|
||||
@@ -443,39 +398,31 @@
|
||||
[:before-path [:maybe [:vector :string]]]
|
||||
[:before-group? [:maybe :boolean]]]]
|
||||
|
||||
[:del-token-set
|
||||
[:map {:title "DelTokenSetChange"}
|
||||
[:type [:= :del-token-set]]
|
||||
[:name :string]]]
|
||||
|
||||
[:del-token-set-path
|
||||
[:map {:title "DelTokenSetPathChange"}
|
||||
[:type [:= :del-token-set-path]]
|
||||
[:path :string]]]
|
||||
[:set-token-theme
|
||||
[:map {:title "SetTokenThemeChange"}
|
||||
[:type [:= :set-token-theme]]
|
||||
[:theme-name :string]
|
||||
[:group :string]
|
||||
[:theme [:maybe ::ctot/token-theme]]]]
|
||||
|
||||
[:set-tokens-lib
|
||||
[:map {:title "SetTokensLib"}
|
||||
[:type [:= :set-tokens-lib]]
|
||||
[:tokens-lib :any]]]
|
||||
|
||||
[:add-token
|
||||
[:map {:title "AddTokenChange"}
|
||||
[:type [:= :add-token]]
|
||||
[:set-token-set
|
||||
[:map {:title "SetTokenSetChange"}
|
||||
[:type [:= :set-token-set]]
|
||||
[:set-name :string]
|
||||
[:token ::cto/token]]]
|
||||
[:group? :boolean]
|
||||
[:token-set [:maybe ::ctot/token-set]]]]
|
||||
|
||||
[:mod-token
|
||||
[:map {:title "ModTokenChange"}
|
||||
[:type [:= :mod-token]]
|
||||
[:set-token
|
||||
[:map {:title "SetTokenChange"}
|
||||
[:type [:= :set-token]]
|
||||
[:set-name :string]
|
||||
[:name :string]
|
||||
[:token ::cto/token]]]
|
||||
|
||||
[:del-token
|
||||
[:map {:title "DelTokenChange"}
|
||||
[:type [:= :del-token]]
|
||||
[:set-name :string]
|
||||
[:name :string]]]]])
|
||||
[:token-name :string]
|
||||
[:token [:maybe ::cto/token]]]]]])
|
||||
|
||||
(def schema:changes
|
||||
[:sequential {:gen/max 5 :gen/min 1} schema:change])
|
||||
@@ -1040,80 +987,63 @@
|
||||
[data {:keys [tokens-lib]}]
|
||||
(assoc data :tokens-lib tokens-lib))
|
||||
|
||||
(defmethod process-change :add-token
|
||||
[data {:keys [set-name token]}]
|
||||
(update data :tokens-lib #(-> %
|
||||
(ctob/ensure-tokens-lib)
|
||||
(ctob/add-token-in-set set-name (ctob/make-token token)))))
|
||||
(defmethod process-change :set-token
|
||||
[data {:keys [set-name token-name token]}]
|
||||
(update data :tokens-lib
|
||||
(fn [lib]
|
||||
(let [lib' (ctob/ensure-tokens-lib lib)]
|
||||
(cond
|
||||
(not token)
|
||||
(ctob/delete-token-from-set lib' set-name token-name)
|
||||
|
||||
(defmethod process-change :mod-token
|
||||
[data {:keys [set-name name token]}]
|
||||
(update data :tokens-lib #(-> %
|
||||
(ctob/ensure-tokens-lib)
|
||||
(ctob/update-token-in-set
|
||||
set-name
|
||||
name
|
||||
(fn [old-token]
|
||||
(ctob/make-token (merge old-token token)))))))
|
||||
(not (ctob/get-token-in-set lib' set-name token-name))
|
||||
(ctob/add-token-in-set lib' set-name (ctob/make-token token))
|
||||
|
||||
(defmethod process-change :del-token
|
||||
[data {:keys [set-name name]}]
|
||||
(update data :tokens-lib #(-> %
|
||||
(ctob/ensure-tokens-lib)
|
||||
(ctob/delete-token-from-set
|
||||
set-name
|
||||
name))))
|
||||
:else
|
||||
(ctob/update-token-in-set lib' set-name token-name (fn [prev-token]
|
||||
(ctob/make-token (merge prev-token token)))))))))
|
||||
|
||||
(defmethod process-change :add-temporary-token-theme
|
||||
[data {:keys [token-theme]}]
|
||||
(update data :tokens-lib #(-> %
|
||||
(ctob/ensure-tokens-lib)
|
||||
(ctob/add-theme (ctob/make-token-theme token-theme)))))
|
||||
(defmethod process-change :set-token-set
|
||||
[data {:keys [set-name group? token-set]}]
|
||||
(update data :tokens-lib
|
||||
(fn [lib]
|
||||
(let [lib' (ctob/ensure-tokens-lib lib)]
|
||||
(cond
|
||||
(not token-set)
|
||||
(if group?
|
||||
(ctob/delete-set-group lib' set-name)
|
||||
(ctob/delete-set lib' set-name))
|
||||
|
||||
(not (ctob/get-set lib' set-name))
|
||||
(ctob/add-set lib' (ctob/make-token-set token-set))
|
||||
|
||||
:else
|
||||
(ctob/update-set lib' set-name (fn [prev-token-set]
|
||||
(ctob/make-token-set (merge prev-token-set token-set)))))))))
|
||||
|
||||
(defmethod process-change :set-token-theme
|
||||
[data {:keys [group theme-name theme]}]
|
||||
(update data :tokens-lib
|
||||
(fn [lib]
|
||||
(let [lib' (ctob/ensure-tokens-lib lib)]
|
||||
(cond
|
||||
(not theme)
|
||||
(ctob/delete-theme lib' group theme-name)
|
||||
|
||||
(not (ctob/get-theme lib' group theme-name))
|
||||
(ctob/add-theme lib' (ctob/make-token-theme theme))
|
||||
|
||||
:else
|
||||
(ctob/update-theme lib'
|
||||
group theme-name
|
||||
(fn [prev-token-theme]
|
||||
(ctob/make-token-theme (merge prev-token-theme theme)))))))))
|
||||
|
||||
(defmethod process-change :update-active-token-themes
|
||||
[data {:keys [theme-ids]}]
|
||||
(update data :tokens-lib #(-> % (ctob/ensure-tokens-lib)
|
||||
(ctob/set-active-themes theme-ids))))
|
||||
|
||||
(defmethod process-change :delete-temporary-token-theme
|
||||
[data {:keys [group name]}]
|
||||
(update data :tokens-lib #(-> %
|
||||
(ctob/ensure-tokens-lib)
|
||||
(ctob/delete-theme group name))))
|
||||
|
||||
(defmethod process-change :add-token-theme
|
||||
[data {:keys [token-theme]}]
|
||||
(update data :tokens-lib #(-> %
|
||||
(ctob/ensure-tokens-lib)
|
||||
(ctob/add-theme (-> token-theme
|
||||
(ctob/make-token-theme))))))
|
||||
|
||||
(defmethod process-change :mod-token-theme
|
||||
[data {:keys [name group token-theme]}]
|
||||
(update data :tokens-lib #(-> %
|
||||
(ctob/ensure-tokens-lib)
|
||||
(ctob/update-theme group name
|
||||
(fn [prev-theme]
|
||||
(merge prev-theme token-theme))))))
|
||||
|
||||
(defmethod process-change :del-token-theme
|
||||
[data {:keys [group name]}]
|
||||
(update data :tokens-lib #(-> %
|
||||
(ctob/ensure-tokens-lib)
|
||||
(ctob/delete-theme group name))))
|
||||
|
||||
(defmethod process-change :add-token-set
|
||||
[data {:keys [token-set]}]
|
||||
(update data :tokens-lib #(-> %
|
||||
(ctob/ensure-tokens-lib)
|
||||
(ctob/add-set (ctob/make-token-set token-set)))))
|
||||
|
||||
(defmethod process-change :add-token-sets
|
||||
[data {:keys [token-sets]}]
|
||||
(update data :tokens-lib #(-> %
|
||||
(ctob/ensure-tokens-lib)
|
||||
(ctob/add-sets (map ctob/make-token-set token-sets)))))
|
||||
|
||||
(defmethod process-change :rename-token-set-group
|
||||
[data {:keys [set-group-path set-group-fname]}]
|
||||
(update data :tokens-lib (fn [lib]
|
||||
@@ -1121,14 +1051,6 @@
|
||||
(ctob/ensure-tokens-lib)
|
||||
(ctob/rename-set-group set-group-path set-group-fname)))))
|
||||
|
||||
(defmethod process-change :mod-token-set
|
||||
[data {:keys [name token-set]}]
|
||||
(update data :tokens-lib (fn [lib]
|
||||
(-> lib
|
||||
(ctob/ensure-tokens-lib)
|
||||
(ctob/update-set name (fn [prev-set]
|
||||
(merge prev-set (dissoc token-set :tokens))))))))
|
||||
|
||||
(defmethod process-change :move-token-set-before
|
||||
[data {:keys [from-path to-path before-path before-group?] :as changes}]
|
||||
(update data :tokens-lib #(-> %
|
||||
@@ -1141,18 +1063,6 @@
|
||||
(ctob/ensure-tokens-lib)
|
||||
(ctob/move-set-group from-path to-path before-path before-group?))))
|
||||
|
||||
(defmethod process-change :del-token-set
|
||||
[data {:keys [name]}]
|
||||
(update data :tokens-lib #(-> %
|
||||
(ctob/ensure-tokens-lib)
|
||||
(ctob/delete-set-path name))))
|
||||
|
||||
(defmethod process-change :del-token-set-path
|
||||
[data {:keys [path]}]
|
||||
(update data :tokens-lib #(-> %
|
||||
(ctob/ensure-tokens-lib)
|
||||
(ctob/delete-set-path path))))
|
||||
|
||||
;; === Operations
|
||||
|
||||
(def ^:private decode-shape
|
||||
|
||||
@@ -155,13 +155,14 @@
|
||||
(dm/get-in data [:pages-index uuid/zero :objects])))
|
||||
|
||||
(defn apply-changes-local
|
||||
[changes]
|
||||
[changes & {:keys [apply-to-library?]}]
|
||||
(dm/assert!
|
||||
"expected valid changes"
|
||||
(check-changes! changes))
|
||||
|
||||
(if-let [file-data (::file-data (meta changes))]
|
||||
(let [index (::applied-changes-count (meta changes))
|
||||
(let [library-data (::library-data (meta changes))
|
||||
index (::applied-changes-count (meta changes))
|
||||
redo-changes (:redo-changes changes)
|
||||
new-changes (if (< index (count redo-changes))
|
||||
(->> (subvec (:redo-changes changes) index)
|
||||
@@ -169,8 +170,12 @@
|
||||
(assoc :page-id uuid/zero)
|
||||
(dissoc :component-id))))
|
||||
[])
|
||||
new-file-data (cfc/process-changes file-data new-changes)]
|
||||
new-file-data (cfc/process-changes file-data new-changes)
|
||||
new-library-data (if apply-to-library?
|
||||
(cfc/process-changes library-data new-changes)
|
||||
library-data)]
|
||||
(vary-meta changes assoc ::file-data new-file-data
|
||||
::library-data new-library-data
|
||||
::applied-changes-count (count redo-changes)))
|
||||
changes))
|
||||
|
||||
@@ -762,13 +767,6 @@
|
||||
(update :undo-changes conj {:type :add-typography :typography prev-typography})
|
||||
(apply-changes-local))))
|
||||
|
||||
(defn add-temporary-token-theme
|
||||
[changes token-theme]
|
||||
(-> changes
|
||||
(update :redo-changes conj {:type :add-temporary-token-theme :token-theme token-theme})
|
||||
(update :undo-changes conj {:type :delete-temporary-token-theme :id (:id token-theme) :name (:name token-theme)})
|
||||
(apply-changes-local)))
|
||||
|
||||
(defn update-active-token-themes
|
||||
[changes token-active-theme-ids prev-token-active-theme-ids]
|
||||
(-> changes
|
||||
@@ -776,42 +774,32 @@
|
||||
(update :undo-changes conj {:type :update-active-token-themes :theme-ids prev-token-active-theme-ids})
|
||||
(apply-changes-local)))
|
||||
|
||||
(defn add-token-theme
|
||||
[changes token-theme]
|
||||
(-> changes
|
||||
(update :redo-changes conj {:type :add-token-theme :token-theme token-theme})
|
||||
(update :undo-changes conj {:type :del-token-theme :group (:group token-theme) :name (:name token-theme)})
|
||||
(apply-changes-local)))
|
||||
|
||||
(defn update-token-theme
|
||||
[changes token-theme prev-token-theme]
|
||||
(let [name (or (:name prev-token-theme)
|
||||
(:name token-theme))
|
||||
group (or (:group prev-token-theme)
|
||||
(:group token-theme))]
|
||||
(-> changes
|
||||
(update :redo-changes conj {:type :mod-token-theme :group group :name name :token-theme token-theme})
|
||||
(update :undo-changes conj {:type :mod-token-theme :group group :name name :token-theme (or prev-token-theme token-theme)})
|
||||
(apply-changes-local))))
|
||||
|
||||
(defn delete-token-theme
|
||||
[changes group name]
|
||||
(defn set-token-theme [changes group theme-name theme]
|
||||
(assert-library! changes)
|
||||
(let [library-data (::library-data (meta changes))
|
||||
prev-token-theme (some-> (get library-data :tokens-lib)
|
||||
(ctob/get-theme group name))]
|
||||
prev-theme (some-> (get library-data :tokens-lib)
|
||||
(ctob/get-theme group theme-name))]
|
||||
(-> changes
|
||||
(update :redo-changes conj {:type :del-token-theme :group group :name name})
|
||||
(update :undo-changes conj {:type :add-token-theme :token-theme prev-token-theme})
|
||||
(update :redo-changes conj {:type :set-token-theme
|
||||
:theme-name theme-name
|
||||
:group group
|
||||
:theme theme})
|
||||
(update :undo-changes conj (if prev-theme
|
||||
{:type :set-token-theme
|
||||
:group group
|
||||
:theme-name (or
|
||||
;; Undo of edit
|
||||
(:name theme)
|
||||
;; Undo of delete
|
||||
theme-name)
|
||||
:theme prev-theme}
|
||||
;; Undo of create
|
||||
{:type :set-token-theme
|
||||
:group group
|
||||
:theme-name theme-name
|
||||
:theme nil}))
|
||||
(apply-changes-local))))
|
||||
|
||||
(defn add-token-set
|
||||
[changes token-set]
|
||||
(-> changes
|
||||
(update :redo-changes conj {:type :add-token-set :token-set token-set})
|
||||
(update :undo-changes conj {:type :del-token-set :name (:name token-set)})
|
||||
(apply-changes-local)))
|
||||
|
||||
(defn rename-token-set-group
|
||||
[changes set-group-path set-group-fname]
|
||||
(let [undo-path (ctob/replace-last-path-name set-group-path set-group-fname)
|
||||
@@ -821,29 +809,6 @@
|
||||
(update :undo-changes conj {:type :rename-token-set-group :set-group-path undo-path :set-group-fname undo-fname})
|
||||
(apply-changes-local))))
|
||||
|
||||
(defn update-token-set
|
||||
[changes token-set prev-token-set]
|
||||
(-> changes
|
||||
(update :redo-changes conj {:type :mod-token-set :name (:name prev-token-set) :token-set token-set})
|
||||
(update :undo-changes conj {:type :mod-token-set :name (:name token-set) :token-set (or prev-token-set token-set)})
|
||||
(apply-changes-local)))
|
||||
|
||||
(defn delete-token-set-path
|
||||
[changes group? path]
|
||||
(assert-library! changes)
|
||||
(let [;; TODO Move leaking prefix to library
|
||||
prefixed-path (if group?
|
||||
(ctob/set-group-path->set-group-prefixed-path path)
|
||||
(ctob/set-full-path->set-prefixed-full-path path))
|
||||
prefixed-path-str (ctob/join-set-path prefixed-path)
|
||||
library-data (::library-data (meta changes))
|
||||
prev-token-sets (some-> (get library-data :tokens-lib)
|
||||
(ctob/get-path-sets prefixed-path-str))]
|
||||
(-> changes
|
||||
(update :redo-changes conj {:type :del-token-set-path :path prefixed-path-str})
|
||||
(update :undo-changes conj {:type :add-token-sets :token-sets prev-token-sets})
|
||||
(apply-changes-local))))
|
||||
|
||||
(defn move-token-set-before
|
||||
[changes {:keys [from-path to-path before-path before-group? prev-before-path prev-before-group?] :as opts}]
|
||||
(-> changes
|
||||
@@ -861,7 +826,6 @@
|
||||
|
||||
(defn move-token-set-group-before
|
||||
[changes {:keys [from-path to-path before-path before-group? prev-before-path prev-before-group?]}]
|
||||
(prn prev-before-path prev-before-group?)
|
||||
(-> changes
|
||||
(update :redo-changes conj {:type :move-token-set-group-before
|
||||
:from-path from-path
|
||||
@@ -884,36 +848,84 @@
|
||||
(update :undo-changes conj {:type :set-tokens-lib :tokens-lib prev-tokens-lib})
|
||||
(apply-changes-local))))
|
||||
|
||||
(defn add-token
|
||||
[changes set-name token]
|
||||
(-> changes
|
||||
(update :redo-changes conj {:type :add-token :set-name set-name :token token})
|
||||
(update :undo-changes conj {:type :del-token :set-name set-name :name (:name token)})
|
||||
(apply-changes-local)))
|
||||
|
||||
(defn update-token
|
||||
[changes set-name token prev-token]
|
||||
(-> changes
|
||||
(update :redo-changes conj {:type :mod-token :set-name set-name :name (:name prev-token) :token token})
|
||||
(update :undo-changes conj {:type :mod-token :set-name set-name :name (:name token) :token (or prev-token token)})
|
||||
(apply-changes-local)))
|
||||
|
||||
(defn delete-token
|
||||
[changes set-name token-name]
|
||||
(defn set-token [changes set-name token-name token]
|
||||
(assert-library! changes)
|
||||
(let [library-data (::library-data (meta changes))
|
||||
prev-token (some-> (get library-data :tokens-lib)
|
||||
(ctob/get-set set-name)
|
||||
(ctob/get-token token-name))]
|
||||
(-> changes
|
||||
(update :redo-changes conj {:type :del-token :set-name set-name :name token-name})
|
||||
(update :undo-changes conj {:type :add-token :set-name set-name :token prev-token})
|
||||
(update :redo-changes conj {:type :set-token
|
||||
:set-name set-name
|
||||
:token-name token-name
|
||||
:token token})
|
||||
(update :undo-changes conj (if prev-token
|
||||
{:type :set-token
|
||||
:set-name set-name
|
||||
:token-name (or
|
||||
;; Undo of edit
|
||||
(:name token)
|
||||
;; Undo of delete
|
||||
token-name)
|
||||
:token prev-token}
|
||||
;; Undo of create token
|
||||
{:type :set-token
|
||||
:set-name set-name
|
||||
:token-name token-name
|
||||
:token nil}))
|
||||
(apply-changes-local))))
|
||||
|
||||
(defn rename-token-set
|
||||
[changes name new-name]
|
||||
|
||||
(assert-library! changes)
|
||||
(let [library-data (::library-data (meta changes))
|
||||
prev-token-set (some-> (get library-data :tokens-lib)
|
||||
(ctob/get-set name))]
|
||||
(-> changes
|
||||
(update :redo-changes conj {:type :set-token-set
|
||||
:set-name name
|
||||
:token-set (assoc prev-token-set :name new-name)
|
||||
:group? false})
|
||||
(update :undo-changes conj {:type :set-token-set
|
||||
:set-name new-name
|
||||
:token-set prev-token-set
|
||||
:group? false})
|
||||
(apply-changes-local))))
|
||||
|
||||
(defn set-token-set
|
||||
[changes set-name group? token-set]
|
||||
(assert-library! changes)
|
||||
(let [library-data (::library-data (meta changes))
|
||||
prev-token-set (some-> (get library-data :tokens-lib)
|
||||
(ctob/get-set set-name))]
|
||||
(-> changes
|
||||
(update :redo-changes conj {:type :set-token-set
|
||||
:set-name set-name
|
||||
:token-set token-set
|
||||
:group? group?})
|
||||
(update :undo-changes conj (if prev-token-set
|
||||
{:type :set-token-set
|
||||
:set-name (or
|
||||
;; Undo of edit
|
||||
(:name token-set)
|
||||
;; Undo of delete
|
||||
set-name)
|
||||
:token-set prev-token-set
|
||||
:group? group?}
|
||||
;; Undo of create
|
||||
{:type :set-token-set
|
||||
:set-name set-name
|
||||
:token-set nil
|
||||
:group? group?}))
|
||||
(apply-changes-local))))
|
||||
|
||||
(defn add-component
|
||||
([changes id path name new-shapes updated-shapes main-instance-id main-instance-page]
|
||||
(add-component changes id path name new-shapes updated-shapes main-instance-id main-instance-page nil))
|
||||
(add-component changes id path name new-shapes updated-shapes main-instance-id main-instance-page nil nil nil))
|
||||
([changes id path name new-shapes updated-shapes main-instance-id main-instance-page annotation]
|
||||
(add-component changes id path name new-shapes updated-shapes main-instance-id main-instance-page annotation nil nil))
|
||||
([changes id path name new-shapes updated-shapes main-instance-id main-instance-page annotation variant-id variant-properties & {:keys [apply-changes-local-library?]}]
|
||||
(assert-page-id! changes)
|
||||
(assert-objects! changes)
|
||||
(let [page-id (::page-id (meta changes))
|
||||
@@ -952,7 +964,9 @@
|
||||
:name name
|
||||
:main-instance-id main-instance-id
|
||||
:main-instance-page main-instance-page
|
||||
:annotation annotation}
|
||||
:annotation annotation
|
||||
:variant-id variant-id
|
||||
:variant-properties variant-properties}
|
||||
(some? new-shapes) ;; this will be null in components-v2
|
||||
(assoc :shapes (vec new-shapes))))
|
||||
(into (map mk-change) updated-shapes))))
|
||||
@@ -967,10 +981,10 @@
|
||||
(map mk-change))
|
||||
updated-shapes))))
|
||||
|
||||
(apply-changes-local)))))
|
||||
(apply-changes-local {:apply-to-library? apply-changes-local-library?})))))
|
||||
|
||||
(defn update-component
|
||||
[changes id update-fn]
|
||||
[changes id update-fn & {:keys [apply-changes-local-library?]}]
|
||||
(assert-library! changes)
|
||||
(let [library-data (::library-data (meta changes))
|
||||
prev-component (get-in library-data [:components id])
|
||||
@@ -984,6 +998,8 @@
|
||||
:main-instance-id (:main-instance-id new-component)
|
||||
:main-instance-page (:main-instance-page new-component)
|
||||
:annotation (:annotation new-component)
|
||||
:variant-id (:variant-id new-component)
|
||||
:variant-properties (:variant-properties new-component)
|
||||
:objects (:objects new-component) ;; this won't exist in components-v2 (except for deleted components)
|
||||
:modified-at (:modified-at new-component)})
|
||||
(update :undo-changes conj {:type :mod-component
|
||||
@@ -993,7 +1009,11 @@
|
||||
:main-instance-id (:main-instance-id prev-component)
|
||||
:main-instance-page (:main-instance-page prev-component)
|
||||
:annotation (:annotation prev-component)
|
||||
:objects (:objects prev-component)}))
|
||||
:variant-id (:variant-id prev-component)
|
||||
:variant-properties (:variant-properties prev-component)
|
||||
:objects (:objects prev-component)})
|
||||
(cond-> apply-changes-local-library?
|
||||
(apply-changes-local {:apply-to-library? true})))
|
||||
changes)))
|
||||
|
||||
(defn delete-component
|
||||
@@ -1055,3 +1075,11 @@
|
||||
(reduce reorder-grid changes))]
|
||||
|
||||
changes))
|
||||
|
||||
(defn get-library-data
|
||||
[changes]
|
||||
(::library-data (meta changes)))
|
||||
|
||||
(defn get-objects
|
||||
[changes]
|
||||
(dm/get-in (::file-data (meta changes)) [:pages-index uuid/zero :objects]))
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.geom.shapes.common :as gco]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.uuid :as uuid]
|
||||
[clojure.set :as set]
|
||||
[clojure.walk :as walk]
|
||||
@@ -201,7 +200,7 @@
|
||||
result))))
|
||||
|
||||
(defn get-parent-seq
|
||||
"Returns a vector of parents of the specified shape."
|
||||
"Returns a lazy seq of parents of the specified shape."
|
||||
([objects shape-id]
|
||||
(get-parent-seq objects (get objects shape-id) shape-id))
|
||||
|
||||
@@ -400,31 +399,51 @@
|
||||
elements)]
|
||||
(into #{} (keep :name) elements)))
|
||||
|
||||
(defn- extract-numeric-suffix
|
||||
[basename]
|
||||
(if-let [[_ p1 p2] (re-find #"(.*) ([0-9]+)$" basename)]
|
||||
[p1 (+ 1 (d/parse-integer p2))]
|
||||
[basename 1]))
|
||||
(defn- name-seq
|
||||
"Creates a lazy, infinite sequence of names starting with `base-name`,
|
||||
followed by variants with suffixes applied. The sequence follows this pattern:
|
||||
- `base-name`
|
||||
- `(str base-name (suffix-fn 1))`
|
||||
- `(str base-name (suffix-fn 2))`
|
||||
- `(str base-name (suffix-fn 3))`, etc."
|
||||
[base-name suffix-fn]
|
||||
(cons base-name
|
||||
(map #(str/concat base-name (suffix-fn %))
|
||||
(iterate inc 1))))
|
||||
|
||||
(defn ^:private get-suffix
|
||||
"Default suffix impelemtation"
|
||||
[copy-count]
|
||||
(str/concat " " copy-count))
|
||||
|
||||
(defn generate-unique-name
|
||||
"A unique name generator"
|
||||
[used basename]
|
||||
"Generates a unique name by selecting the first available name from a generated sequence.
|
||||
The sequence consists of `base-name` and its variants, avoiding conflicts with `existing-names`.
|
||||
|
||||
Parameters:
|
||||
- `base-name` - string used as the base for name generation.
|
||||
- `existing-names` - a collection of existing names to check for uniqueness.
|
||||
- Options:
|
||||
- `:suffix-fn` - a function that generates suffixes, given an integer (default: `get-suffix`).
|
||||
- `:immediate-suffix?` - if `true`, the base name is considered taken, and suffixing starts immediately.
|
||||
|
||||
Returns:
|
||||
- A unique name not present in `existing-names`."
|
||||
[base-name existing-names & {:keys [suffix-fn immediate-suffix?]
|
||||
:or {suffix-fn get-suffix}}]
|
||||
(dm/assert!
|
||||
"expected a set of strings"
|
||||
(sm/check-set-of-strings! used))
|
||||
(coll? existing-names))
|
||||
|
||||
(dm/assert!
|
||||
"expected a string for `basename`."
|
||||
(string? basename))
|
||||
|
||||
(if-not (contains? used basename)
|
||||
basename
|
||||
(let [[prefix initial] (extract-numeric-suffix basename)]
|
||||
(loop [counter initial]
|
||||
(let [candidate (str prefix " " counter)]
|
||||
(if (contains? used candidate)
|
||||
(recur (inc counter))
|
||||
candidate))))))
|
||||
(string? base-name))
|
||||
(let [existing-name-set (cond-> (set existing-names)
|
||||
immediate-suffix? (conj base-name))
|
||||
names (name-seq base-name suffix-fn)]
|
||||
(->> names
|
||||
(remove #(contains? existing-name-set %))
|
||||
first)))
|
||||
|
||||
(defn walk-pages
|
||||
"Go through all pages of a file and apply a function to each one"
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
[app.common.types.file :as ctf]
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.types.shape.shadow :as ctss]
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
[app.common.uuid :as uuid]
|
||||
[clojure.set :as set]
|
||||
[cuerdas.core :as str]))
|
||||
@@ -1225,6 +1226,46 @@
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
|
||||
(defmethod migrate-data "Ensure hidden theme"
|
||||
[data _]
|
||||
(letfn [(update-tokens-lib [tokens-lib]
|
||||
(let [hidden-theme (ctob/get-hidden-theme tokens-lib)]
|
||||
(if (nil? hidden-theme)
|
||||
(ctob/add-theme tokens-lib (ctob/make-hidden-token-theme))
|
||||
tokens-lib)))]
|
||||
(if (contains? data :tokens-lib)
|
||||
(update data :tokens-lib update-tokens-lib)
|
||||
data)))
|
||||
|
||||
(defmethod migrate-data "Add token theme id"
|
||||
[data _]
|
||||
(letfn [(update-tokens-lib [tokens-lib]
|
||||
(let [themes (ctob/get-themes tokens-lib)]
|
||||
(reduce (fn [lib theme]
|
||||
(if (:id theme)
|
||||
lib
|
||||
(ctob/update-theme lib (:group theme) (:name theme) #(assoc % :id (str (uuid/next))))))
|
||||
tokens-lib
|
||||
themes)))]
|
||||
(if (contains? data :tokens-lib)
|
||||
(update data :tokens-lib update-tokens-lib)
|
||||
data)))
|
||||
|
||||
(defmethod migrate-data "Remove tokens from groups"
|
||||
[data _]
|
||||
(letfn [(update-object [object]
|
||||
(cond-> object
|
||||
(and (= :group (:type object))
|
||||
(contains? (:applied-tokens object) :fill))
|
||||
(assoc :fills [])
|
||||
(and (= :group (:type object))
|
||||
(contains? object :applied-tokens))
|
||||
(dissoc :applied-tokens)))
|
||||
|
||||
(update-page [page]
|
||||
(d/update-when page :objects update-vals update-object))]
|
||||
(update data :pages-index update-vals update-page)))
|
||||
|
||||
(def available-migrations
|
||||
(into (d/ordered-set)
|
||||
["legacy-2"
|
||||
@@ -1278,4 +1319,7 @@
|
||||
"legacy-62"
|
||||
"legacy-65"
|
||||
"legacy-66"
|
||||
"legacy-67"]))
|
||||
"legacy-67"
|
||||
"Ensure hidden theme"
|
||||
"Add token theme id"
|
||||
"Remove tokens from groups"]))
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
[shape changes]))
|
||||
|
||||
(defn prepare-move-shapes-into-frame
|
||||
[changes frame-id shapes objects]
|
||||
[changes frame-id shapes objects remove-layout-data?]
|
||||
(let [parent-id (dm/get-in objects [frame-id :parent-id])
|
||||
shapes (remove #(= % parent-id) shapes)
|
||||
to-move (->> shapes
|
||||
@@ -46,7 +46,8 @@
|
||||
(not-empty))]
|
||||
(if to-move
|
||||
(-> changes
|
||||
(cond-> (not (ctl/any-layout? objects frame-id))
|
||||
(cond-> (and remove-layout-data?
|
||||
(not (ctl/any-layout? objects frame-id)))
|
||||
(pcb/update-shapes shapes ctl/remove-layout-item-data))
|
||||
(pcb/update-shapes shapes #(cond-> % (cfh/frame-shape? %) (assoc :hide-in-viewer true)))
|
||||
(pcb/change-parent frame-id to-move 0)
|
||||
@@ -133,7 +134,7 @@
|
||||
(prepare-add-shape changes shape objects)
|
||||
|
||||
changes
|
||||
(prepare-move-shapes-into-frame changes (:id shape) selected' objects)
|
||||
(prepare-move-shapes-into-frame changes (:id shape) selected' objects false)
|
||||
|
||||
changes
|
||||
(cond-> changes
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.common.logic.libraries
|
||||
#?(:cljs (:require-macros [app.common.logic.libraries :refer [shape-log container-log]]))
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
@@ -35,6 +36,33 @@
|
||||
;; Change this to :info :debug or :trace to debug this module, or :warn to reset to default
|
||||
(log/set-level! :warn)
|
||||
|
||||
(def log-shape-ids #{})
|
||||
(def log-container-ids #{})
|
||||
|
||||
(defn enabled-shape?
|
||||
[id container]
|
||||
(or (empty? log-shape-ids)
|
||||
(nil? id)
|
||||
(let [ids (if container
|
||||
(into #{} (cfh/get-parent-ids-seq-with-self (:objects container) id))
|
||||
#{id})]
|
||||
(seq (set/intersection log-shape-ids ids)))))
|
||||
|
||||
(defmacro shape-log
|
||||
[level id container & params]
|
||||
`(when (enabled-shape? ~id ~container)
|
||||
(log/log ~level ~@params)))
|
||||
|
||||
(defn enabled-container?
|
||||
[id]
|
||||
(or (empty? log-container-ids)
|
||||
(log-container-ids id)))
|
||||
|
||||
(defmacro container-log
|
||||
[level id & params]
|
||||
`(when (enabled-container? ~id)
|
||||
(log/log ~level ~@params)))
|
||||
|
||||
(declare generate-sync-container)
|
||||
(declare generate-sync-shape)
|
||||
(declare generate-sync-text-shape)
|
||||
@@ -72,10 +100,10 @@
|
||||
|
||||
;; ---- Components and instances creation ----
|
||||
|
||||
(defn duplicate-component
|
||||
(defn- duplicate-component
|
||||
"Clone the root shape of the component and all children. Generate new
|
||||
ids from all of them."
|
||||
[component new-component-id library-data]
|
||||
[component new-component-id library-data force-id]
|
||||
(let [components-v2 (dm/get-in library-data [:options :components-v2])]
|
||||
(if components-v2
|
||||
(let [main-instance-page (ctf/get-component-page library-data component)
|
||||
@@ -111,7 +139,8 @@
|
||||
(:parent-id main-instance-shape)
|
||||
(:objects main-instance-page)
|
||||
:update-new-shape update-new-shape
|
||||
:update-original-shape update-original-shape)
|
||||
:update-original-shape update-original-shape
|
||||
:force-id force-id)
|
||||
|
||||
remap-frame
|
||||
(fn [shape]
|
||||
@@ -151,7 +180,7 @@
|
||||
|
||||
(defn generate-duplicate-component
|
||||
"Create a new component copied from the one with the given id."
|
||||
[changes library component-id components-v2]
|
||||
[changes library component-id new-component-id components-v2 & {:keys [new-shape-id apply-changes-local-library?]}]
|
||||
(let [component (ctkl/get-component (:data library) component-id)
|
||||
new-name (:name component)
|
||||
|
||||
@@ -159,26 +188,39 @@
|
||||
(ctf/get-component-page (:data library) component))
|
||||
|
||||
new-component-id (when components-v2
|
||||
(uuid/next))
|
||||
new-component-id)
|
||||
|
||||
[new-component-shape new-component-shapes ; <- null in components-v2
|
||||
new-main-instance-shape new-main-instance-shapes]
|
||||
(duplicate-component component new-component-id (:data library))]
|
||||
(duplicate-component component new-component-id (:data library) new-shape-id)]
|
||||
|
||||
[new-main-instance-shape
|
||||
(-> changes
|
||||
(pcb/with-page main-instance-page)
|
||||
(pcb/with-objects (:objects main-instance-page))
|
||||
(pcb/add-objects new-main-instance-shapes {:ignore-touched true})
|
||||
(pcb/add-component (if components-v2
|
||||
new-component-id
|
||||
(:id new-component-shape))
|
||||
(:path component)
|
||||
new-name
|
||||
new-component-shapes
|
||||
[]
|
||||
(:id new-main-instance-shape)
|
||||
(:id main-instance-page)
|
||||
(:annotation component)
|
||||
(:variant-id component)
|
||||
(:variant-properties component)
|
||||
{:apply-changes-local-library? apply-changes-local-library?})
|
||||
;; Update grid layout if the new main instance is inside
|
||||
(pcb/update-shapes
|
||||
[(:frame-id new-main-instance-shape)]
|
||||
(fn [shape objects]
|
||||
(cond-> shape
|
||||
(ctl/grid-layout? shape)
|
||||
(ctl/assign-cells objects)))
|
||||
{:with-objects? true}))]))
|
||||
|
||||
(-> changes
|
||||
(pcb/with-page main-instance-page)
|
||||
(pcb/with-objects (:objects main-instance-page))
|
||||
(pcb/add-objects new-main-instance-shapes {:ignore-touched true})
|
||||
(pcb/add-component (if components-v2
|
||||
new-component-id
|
||||
(:id new-component-shape))
|
||||
(:path component)
|
||||
new-name
|
||||
new-component-shapes
|
||||
[]
|
||||
(:id new-main-instance-shape)
|
||||
(:id main-instance-page)
|
||||
(:annotation component)))))
|
||||
|
||||
(defn generate-instantiate-component
|
||||
"Generate changes to create a new instance from a component."
|
||||
@@ -257,7 +299,8 @@
|
||||
with a component."
|
||||
[changes container libraries shape-id]
|
||||
(let [shape (ctn/get-shape container shape-id)]
|
||||
(log/debug :msg "Detach instance" :shape-id shape-id :container (:id container))
|
||||
(shape-log :debug shape-id container
|
||||
:msg "Detach instance" :shape-id shape-id :container (:id container))
|
||||
(generate-detach-recursive changes container libraries shape-id true (true? (:component-root shape)))))
|
||||
|
||||
(defn- generate-detach-recursive
|
||||
@@ -374,11 +417,12 @@
|
||||
(s/assert ::us/uuid file-id)
|
||||
(s/assert ::us/uuid library-id)
|
||||
|
||||
(log/info :msg "Sync file with library"
|
||||
:asset-type asset-type
|
||||
:asset-id asset-id
|
||||
:file (pretty-file file-id libraries current-file-id)
|
||||
:library (pretty-file library-id libraries current-file-id))
|
||||
(container-log :info asset-id
|
||||
:msg "Sync file with library"
|
||||
:asset-type asset-type
|
||||
:asset-id asset-id
|
||||
:file (pretty-file file-id libraries current-file-id)
|
||||
:library (pretty-file library-id libraries current-file-id))
|
||||
|
||||
(let [file (get-in libraries [file-id :data])
|
||||
components-v2 (get-in file [:options :components-v2])]
|
||||
@@ -412,11 +456,12 @@
|
||||
(s/assert ::us/uuid file-id)
|
||||
(s/assert ::us/uuid library-id)
|
||||
|
||||
(log/info :msg "Sync local components with library"
|
||||
:asset-type asset-type
|
||||
:asset-id asset-id
|
||||
:file (pretty-file file-id libraries current-file-id)
|
||||
:library (pretty-file library-id libraries current-file-id))
|
||||
(container-log :info asset-id
|
||||
:msg "Sync local components with library"
|
||||
:asset-type asset-type
|
||||
:asset-id asset-id
|
||||
:file (pretty-file file-id libraries current-file-id)
|
||||
:library (pretty-file library-id libraries current-file-id))
|
||||
|
||||
(let [file (get-in libraries [file-id :data])
|
||||
components-v2 (get-in file [:options :components-v2])]
|
||||
@@ -442,8 +487,8 @@
|
||||
[changes asset-type asset-id library-id container components-v2 libraries current-file-id]
|
||||
|
||||
(if (cfh/page? container)
|
||||
(log/debug :msg "Sync page in local file" :page-id (:id container))
|
||||
(log/debug :msg "Sync component in local library" :component-id (:id container)))
|
||||
(container-log :debug (:id container) :msg "Sync page in local file" :page-id (:id container))
|
||||
(container-log :debug (:id container) :msg "Sync component in local library" :component-id (:id container)))
|
||||
|
||||
(let [linked-shapes (->> (vals (:objects container))
|
||||
(filter #(uses-assets? asset-type asset-id % library-id)))]
|
||||
@@ -498,7 +543,7 @@
|
||||
|
||||
(defmethod generate-sync-shape :colors
|
||||
[_ changes library-id _ shape _ libraries _]
|
||||
(log/debug :msg "Sync colors of shape" :shape (:name shape))
|
||||
(shape-log :debug (:id shape) nil :msg "Sync colors of shape" :shape (:name shape))
|
||||
|
||||
;; Synchronize a shape that uses some colors of the library. The value of the
|
||||
;; color in the library is copied to the shape.
|
||||
@@ -509,7 +554,7 @@
|
||||
|
||||
(defmethod generate-sync-shape :typographies
|
||||
[_ changes library-id container shape _ libraries _]
|
||||
(log/debug :msg "Sync typographies of shape" :shape (:name shape))
|
||||
(shape-log :debug (:id shape) nil :msg "Sync typographies of shape" :shape (:name shape))
|
||||
|
||||
;; Synchronize a shape that uses some typographies of the library. The attributes
|
||||
;; of the typography are copied to the shape."
|
||||
@@ -671,7 +716,8 @@
|
||||
"Generate changes to synchronize one shape that is the root of a component
|
||||
instance, and all its children, from the given component."
|
||||
[changes file libraries container shape-id reset? components-v2]
|
||||
(log/debug :msg "Sync shape direct" :shape-inst (str shape-id) :reset? reset?)
|
||||
(shape-log :debug shape-id container
|
||||
:msg "Sync shape direct" :shape-inst (str shape-id) :reset? reset?)
|
||||
(let [shape-inst (ctn/get-shape container shape-id)
|
||||
library (dm/get-in libraries [(:component-file shape-inst) :data])
|
||||
component (ctkl/get-component library (:component-id shape-inst) true)]
|
||||
@@ -735,7 +781,8 @@
|
||||
|
||||
(defn- generate-sync-shape-direct-recursive
|
||||
[changes container shape-inst component library file libraries shape-main root-inst root-main reset? initial-root? redirect-shaperef components-v2]
|
||||
(log/debug :msg "Sync shape direct recursive"
|
||||
(shape-log :debug (:id shape-inst) container
|
||||
:msg "Sync shape direct recursive"
|
||||
:shape-inst (str (:name shape-inst) " " (pretty-uuid (:id shape-inst)))
|
||||
:component (:name component))
|
||||
|
||||
@@ -786,7 +833,8 @@
|
||||
(map #(redirect-shaperef %) children-inst) children-inst)
|
||||
|
||||
only-inst (fn [changes child-inst]
|
||||
(log/trace :msg "Only inst"
|
||||
(shape-log :trace (:id child-inst) container
|
||||
:msg "Only inst"
|
||||
:child-inst (str (:name child-inst) " " (pretty-uuid (:id child-inst))))
|
||||
(if-not (and omit-touched?
|
||||
(contains? (:touched shape-inst)
|
||||
@@ -798,7 +846,8 @@
|
||||
changes))
|
||||
|
||||
only-main (fn [changes child-main]
|
||||
(log/trace :msg "Only main"
|
||||
(shape-log :trace (:id child-main) component-container
|
||||
:msg "Only main"
|
||||
:child-main (str (:name child-main) " " (pretty-uuid (:id child-main))))
|
||||
(if-not (and omit-touched?
|
||||
(contains? (:touched shape-inst)
|
||||
@@ -817,7 +866,8 @@
|
||||
changes))
|
||||
|
||||
both (fn [changes child-inst child-main]
|
||||
(log/trace :msg "Both"
|
||||
(shape-log :trace (:id child-inst) container
|
||||
:msg "Both"
|
||||
:child-inst (str (:name child-inst) " " (pretty-uuid (:id child-inst)))
|
||||
:child-main (str (:name child-main) " " (pretty-uuid (:id child-main))))
|
||||
(generate-sync-shape-direct-recursive changes
|
||||
@@ -836,14 +886,16 @@
|
||||
components-v2))
|
||||
|
||||
swapped (fn [changes child-inst child-main]
|
||||
(log/trace :msg "Match slot"
|
||||
(shape-log :trace (:id child-inst) container
|
||||
:msg "Match slot"
|
||||
:child-inst (str (:name child-inst) " " (pretty-uuid (:id child-inst)))
|
||||
:child-main (str (:name child-main) " " (pretty-uuid (:id child-main))))
|
||||
;; For now we don't make any sync here.
|
||||
changes)
|
||||
|
||||
moved (fn [changes child-inst child-main]
|
||||
(log/trace :msg "Move"
|
||||
(shape-log :trace (:id child-inst) container
|
||||
:msg "Move"
|
||||
:child-inst (str (:name child-inst) " " (pretty-uuid (:id child-inst)))
|
||||
:child-main (str (:name child-main) " " (pretty-uuid (:id child-main))))
|
||||
(move-shape
|
||||
@@ -856,6 +908,7 @@
|
||||
|
||||
changes
|
||||
(compare-children changes
|
||||
shape-inst
|
||||
children-inst
|
||||
children-main
|
||||
container
|
||||
@@ -906,7 +959,7 @@
|
||||
"Generate changes to update the component a shape is linked to, from
|
||||
the values in the shape and all its children."
|
||||
[changes file libraries container shape-id components-v2]
|
||||
(log/debug :msg "Sync shape inverse" :shape (str shape-id))
|
||||
(shape-log :debug shape-id container :msg "Sync shape inverse" :shape (str 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])
|
||||
@@ -948,7 +1001,8 @@
|
||||
|
||||
(defn- generate-sync-shape-inverse-recursive
|
||||
[changes container shape-inst component library file libraries shape-main root-inst root-main initial-root? redirect-shaperef components-v2]
|
||||
(log/trace :msg "Sync shape inverse recursive"
|
||||
(shape-log :trace (:id shape-inst) container
|
||||
:msg "Sync shape inverse recursive"
|
||||
:shape (str (:name shape-inst))
|
||||
:component (:name component))
|
||||
|
||||
@@ -1041,7 +1095,8 @@
|
||||
components-v2))
|
||||
|
||||
swapped (fn [changes child-inst child-main]
|
||||
(log/trace :msg "Match slot"
|
||||
(shape-log :trace (:id child-inst) container
|
||||
:msg "Match slot"
|
||||
:child-inst (str (:name child-inst) " " (pretty-uuid (:id child-inst)))
|
||||
:child-main (str (:name child-main) " " (pretty-uuid (:id child-main))))
|
||||
;; For now we don't make any sync here.
|
||||
@@ -1058,6 +1113,7 @@
|
||||
|
||||
changes
|
||||
(compare-children changes
|
||||
shape-inst
|
||||
children-inst
|
||||
children-main
|
||||
container
|
||||
@@ -1089,14 +1145,15 @@
|
||||
;; ---- Operation generation helpers ----
|
||||
|
||||
(defn- compare-children
|
||||
[changes children-inst children-main container-inst container-main file libraries only-inst-cb only-main-cb both-cb swapped-cb moved-cb inverse? reset? components-v2]
|
||||
(log/trace :msg "Compare children")
|
||||
[changes shape-inst children-inst children-main container-inst container-main file libraries only-inst-cb only-main-cb both-cb swapped-cb moved-cb inverse? reset? components-v2]
|
||||
(shape-log :trace (:id shape-inst) container-inst :msg "Compare children")
|
||||
(loop [children-inst (seq (or children-inst []))
|
||||
children-main (seq (or children-main []))
|
||||
changes changes]
|
||||
(let [child-inst (first children-inst)
|
||||
child-main (first children-main)]
|
||||
(log/trace :main (str (:name child-main) " " (pretty-uuid (:id child-main)))
|
||||
(shape-log :trace (:id shape-inst) container-inst
|
||||
:main (str (:name child-main) " " (pretty-uuid (:id child-main)))
|
||||
:inst (str (:name child-inst) " " (pretty-uuid (:id child-inst))))
|
||||
(cond
|
||||
(and (nil? child-inst) (nil? child-main))
|
||||
@@ -1159,10 +1216,11 @@
|
||||
|
||||
(defn- add-shape-to-instance
|
||||
[changes component-shape index component-page container root-instance root-main omit-touched? set-remote-synced? components-v2]
|
||||
(log/info :msg (str "ADD [P " (pretty-uuid (:id container)) "] "
|
||||
(:name component-shape)
|
||||
" "
|
||||
(pretty-uuid (:id component-shape))))
|
||||
(shape-log :info (:id component-shape) component-page
|
||||
:msg (str "ADD [P " (pretty-uuid (:id container)) "] "
|
||||
(:name component-shape)
|
||||
" "
|
||||
(pretty-uuid (:id component-shape))))
|
||||
(let [component-parent-shape (ctn/get-shape component-page (:parent-id component-shape))
|
||||
parent-shape (d/seek #(ctk/is-main-of? component-parent-shape % components-v2)
|
||||
(cfh/get-children-with-self (:objects container)
|
||||
@@ -1234,10 +1292,11 @@
|
||||
|
||||
(defn- add-shape-to-main
|
||||
[changes shape index component component-container page root-instance root-main components-v2]
|
||||
(log/info :msg (str "ADD [C " (pretty-uuid (:id component-container)) "] "
|
||||
(:name shape)
|
||||
" "
|
||||
(pretty-uuid (:id shape))))
|
||||
(shape-log :info (:id shape) page
|
||||
:msg (str "ADD [C " (pretty-uuid (:id component-container)) "] "
|
||||
(:name shape)
|
||||
" "
|
||||
(pretty-uuid (:id shape))))
|
||||
(let [parent-shape (ctn/get-shape page (:parent-id shape))
|
||||
component-parent-shape (d/seek #(ctk/is-main-of? % parent-shape components-v2)
|
||||
(cfh/get-children-with-self (:objects component-container)
|
||||
@@ -1337,12 +1396,13 @@
|
||||
|
||||
(defn- remove-shape
|
||||
[changes shape container omit-touched?]
|
||||
(log/info :msg (str "REMOVE-SHAPE "
|
||||
(if (cfh/page? container) "[P " "[C ")
|
||||
(pretty-uuid (:id container)) "] "
|
||||
(:name shape)
|
||||
" "
|
||||
(pretty-uuid (:id shape))))
|
||||
(shape-log :info (:id shape) container
|
||||
:msg (str "REMOVE-SHAPE "
|
||||
(if (cfh/page? container) "[P " "[C ")
|
||||
(pretty-uuid (:id container)) "] "
|
||||
(:name shape)
|
||||
" "
|
||||
(pretty-uuid (:id shape))))
|
||||
(let [objects (get container :objects)
|
||||
parents (cfh/get-parent-ids objects (:id shape))
|
||||
parent (first parents)
|
||||
@@ -1389,16 +1449,17 @@
|
||||
|
||||
(defn- move-shape
|
||||
[changes shape index-before index-after container omit-touched?]
|
||||
(log/info :msg (str "MOVE "
|
||||
(if (cfh/page? container) "[P " "[C ")
|
||||
(pretty-uuid (:id container)) "] "
|
||||
(:name shape)
|
||||
" "
|
||||
(pretty-uuid (:id shape))
|
||||
" "
|
||||
index-before
|
||||
" -> "
|
||||
index-after))
|
||||
(shape-log :info (:id shape) container
|
||||
:msg (str "MOVE "
|
||||
(if (cfh/page? container) "[P " "[C ")
|
||||
(pretty-uuid (:id container)) "] "
|
||||
(:name shape)
|
||||
" "
|
||||
(pretty-uuid (:id shape))
|
||||
" "
|
||||
index-before
|
||||
" -> "
|
||||
index-after))
|
||||
(let [parent (ctn/get-shape container (:parent-id shape))
|
||||
|
||||
changes' (-> changes
|
||||
@@ -1429,13 +1490,14 @@
|
||||
(if (nil? (:shape-ref dest-shape))
|
||||
changes
|
||||
(do
|
||||
(log/info :msg (str "CHANGE-TOUCHED "
|
||||
(if (cfh/page? container) "[P " "[C ")
|
||||
(pretty-uuid (:id container)) "] "
|
||||
(:name dest-shape)
|
||||
" "
|
||||
(pretty-uuid (:id dest-shape)))
|
||||
:options options)
|
||||
(shape-log :info (:id dest-shape) container
|
||||
:msg (str "CHANGE-TOUCHED "
|
||||
(if (cfh/page? container) "[P " "[C ")
|
||||
(pretty-uuid (:id container)) "] "
|
||||
(:name dest-shape)
|
||||
" "
|
||||
(pretty-uuid (:id dest-shape)))
|
||||
:options options)
|
||||
(let [new-touched (cond
|
||||
reset-touched?
|
||||
nil
|
||||
@@ -1471,13 +1533,14 @@
|
||||
(if (nil? (:shape-ref shape))
|
||||
changes
|
||||
(do
|
||||
(log/info :msg (str "CHANGE-REMOTE-SYNCED? "
|
||||
(if (cfh/page? container) "[P " "[C ")
|
||||
(pretty-uuid (:id container)) "] "
|
||||
(:name shape)
|
||||
" "
|
||||
(pretty-uuid (:id shape)))
|
||||
:remote-synced remote-synced?)
|
||||
(shape-log :info (:id shape) container
|
||||
:msg (str "CHANGE-REMOTE-SYNCED? "
|
||||
(if (cfh/page? container) "[P " "[C ")
|
||||
(pretty-uuid (:id container)) "] "
|
||||
(:name shape)
|
||||
" "
|
||||
(pretty-uuid (:id shape)))
|
||||
:remote-synced remote-synced?)
|
||||
(-> changes
|
||||
(update :redo-changes conj (make-change
|
||||
container
|
||||
@@ -1540,16 +1603,17 @@
|
||||
in the destination shape will not be copied."
|
||||
[changes dest-shape origin-shape dest-root origin-root container omit-touched?]
|
||||
|
||||
(log/info :msg (str "SYNC "
|
||||
(:name origin-shape)
|
||||
" "
|
||||
(pretty-uuid (:id origin-shape))
|
||||
" -> "
|
||||
(if (cfh/page? container) "[P " "[C ")
|
||||
(pretty-uuid (:id container)) "] "
|
||||
(:name dest-shape)
|
||||
" "
|
||||
(pretty-uuid (:id dest-shape))))
|
||||
(shape-log :info (:id dest-shape) container
|
||||
:msg (str "SYNC "
|
||||
(:name origin-shape)
|
||||
" "
|
||||
(pretty-uuid (:id origin-shape))
|
||||
" -> "
|
||||
(if (cfh/page? container) "[P " "[C ")
|
||||
(pretty-uuid (:id container)) "] "
|
||||
(:name dest-shape)
|
||||
" "
|
||||
(pretty-uuid (:id dest-shape))))
|
||||
|
||||
(let [;; To synchronize geometry attributes we need to make a prior
|
||||
;; operation, because coordinates are absolute, but we need to
|
||||
@@ -1722,13 +1786,26 @@
|
||||
(pcb/update-shapes
|
||||
[shape-copy-id]
|
||||
(fn [shape-copy objects]
|
||||
(let [ids-map
|
||||
(let [component-page
|
||||
(ctf/get-component-page main-container main-component)
|
||||
|
||||
component-swap-children
|
||||
(->> shape-main
|
||||
:shapes
|
||||
(map #(get (:objects component-page) %))
|
||||
(filter #(some? (ctk/get-swap-slot %)))
|
||||
(group-by ctk/get-swap-slot))
|
||||
|
||||
ids-map
|
||||
(into {}
|
||||
(comp
|
||||
(map #(get objects %))
|
||||
(keep
|
||||
(fn [copy-shape]
|
||||
(let [main-shape (ctf/get-ref-shape main-container main-component copy-shape)]
|
||||
(let [main-shape
|
||||
(if (some? (ctk/get-swap-slot copy-shape))
|
||||
(first (get component-swap-children (ctk/get-swap-slot copy-shape)))
|
||||
(ctf/get-ref-shape main-container main-component copy-shape))]
|
||||
[(:id main-shape) (:id copy-shape)]))))
|
||||
(:shapes shape-copy))
|
||||
|
||||
@@ -1744,7 +1821,8 @@
|
||||
main-cells (-> shape-main (ctl/remap-grid-cells ids-map) :layout-grid-cells)]
|
||||
(-> shape-copy
|
||||
(assoc :layout-grid-cells
|
||||
(ctl/merge-cells copy-cells main-cells omit-touched?)))))
|
||||
(ctl/merge-cells main-cells copy-cells omit-touched?))
|
||||
(ctl/assign-cells objects))))
|
||||
{:ignore-touched true :with-objects? true})))
|
||||
|
||||
(defn- update-grid-main-attrs
|
||||
@@ -2023,7 +2101,7 @@
|
||||
has-flow? (partial ctp/get-frame-flow flows)]
|
||||
|
||||
(reduce (fn [changes frame-id]
|
||||
(let [name (cfh/generate-unique-name @unames "Flow 1")
|
||||
(let [name (cfh/generate-unique-name "Flow" @unames :immediate-suffix? true)
|
||||
frame-id (get ids-map frame-id)
|
||||
flow-id (uuid/next)
|
||||
new-flow {:id flow-id
|
||||
|
||||
@@ -10,12 +10,14 @@
|
||||
[app.common.files.changes-builder :as pcb]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.logic.variants :as clv]
|
||||
[app.common.types.component :as ctk]
|
||||
[app.common.types.container :as ctn]
|
||||
[app.common.types.shape.interactions :as ctsi]
|
||||
[app.common.types.shape.layout :as ctl]
|
||||
[app.common.types.token :as cto]
|
||||
[app.common.uuid :as uuid]))
|
||||
[app.common.uuid :as uuid]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
(defn- generate-unapply-tokens
|
||||
"When updating attributes that have a token applied, we must unapply it, because the value
|
||||
@@ -239,21 +241,21 @@
|
||||
|
||||
|
||||
(defn generate-relocate
|
||||
[changes objects parent-id page-id to-index ids & {:keys [cell ignore-parents?]}]
|
||||
(let [ids (cfh/order-by-indexed-shapes objects ids)
|
||||
shapes (map (d/getf objects) ids)
|
||||
parent (get objects parent-id)
|
||||
[changes parent-id to-index ids & {:keys [cell ignore-parents?]}]
|
||||
(let [objects (pcb/get-objects changes)
|
||||
ids (cfh/order-by-indexed-shapes objects ids)
|
||||
shapes (map (d/getf objects) ids)
|
||||
parent (get objects parent-id)
|
||||
all-parents (into #{parent-id} (map #(cfh/get-parent-id objects %)) ids)
|
||||
parents (if ignore-parents? #{parent-id} all-parents)
|
||||
parents (if ignore-parents? #{parent-id} all-parents)
|
||||
|
||||
children-ids
|
||||
(->> ids
|
||||
(mapcat #(cfh/get-children-ids-with-self objects %)))
|
||||
children-ids (mapcat #(cfh/get-children-ids-with-self objects %) ids)
|
||||
|
||||
child-heads
|
||||
(->> ids
|
||||
(mapcat #(ctn/get-child-heads objects %))
|
||||
(map :id))
|
||||
child-heads (mapcat #(ctn/get-child-heads objects %) ids)
|
||||
|
||||
child-heads-ids (map :id child-heads)
|
||||
|
||||
variant-heads (filter ctk/is-variant? child-heads)
|
||||
|
||||
component-main-parent
|
||||
(ctn/find-component-main objects parent false)
|
||||
@@ -340,9 +342,6 @@
|
||||
cell (or cell (and index-cell-data [(:row index-cell-data) (:column index-cell-data)]))]
|
||||
|
||||
(-> changes
|
||||
(pcb/with-page-id page-id)
|
||||
(pcb/with-objects objects)
|
||||
|
||||
;; Remove layout-item properties when moving a shape outside a layout
|
||||
(cond-> (not (ctl/any-layout? parent))
|
||||
(pcb/update-shapes ids ctl/remove-layout-item-data))
|
||||
@@ -353,7 +352,7 @@
|
||||
|
||||
;; Remove the swap slots if it is moving to a different component
|
||||
(pcb/update-shapes
|
||||
child-heads
|
||||
child-heads-ids
|
||||
(fn [shape]
|
||||
(cond-> shape
|
||||
(not= component-main-parent (ctn/find-component-main objects shape false))
|
||||
@@ -365,7 +364,86 @@
|
||||
|
||||
;; Add component-root property when moving a component outside a component
|
||||
(cond-> (not (ctn/get-instance-root objects parent))
|
||||
(pcb/update-shapes child-heads #(assoc % :component-root true)))
|
||||
(pcb/update-shapes child-heads-ids #(assoc % :component-root true)))
|
||||
|
||||
;; Remove variant info and rename when moving outside a variant-container
|
||||
(cond-> (not (ctk/is-variant-container? parent))
|
||||
((fn [changes]
|
||||
(reduce
|
||||
(fn [changes shape]
|
||||
(let [new-name (str/replace (:variant-name shape) #", " " / ")
|
||||
[cpath cname] (cfh/parse-path-name new-name)]
|
||||
(-> changes
|
||||
(pcb/update-component (:component-id shape)
|
||||
#(-> (dissoc % :variant-id :variant-properties)
|
||||
(assoc :name cname
|
||||
:path cpath))
|
||||
{:apply-changes-local-library? true})
|
||||
(pcb/update-shapes [(:id shape)]
|
||||
#(-> (dissoc % :variant-id :variant-name)
|
||||
(assoc :name new-name))))))
|
||||
changes
|
||||
variant-heads))))
|
||||
|
||||
;; Add variant info and rename when moving into a different variant-container
|
||||
(cond-> (ctk/is-variant-container? parent)
|
||||
((fn [changes]
|
||||
(let [get-base-name #(if (some? (:variant-name %))
|
||||
(str/replace (:variant-name %) #", " " / ")
|
||||
(:name %))
|
||||
|
||||
calc-num-props #(-> %
|
||||
get-base-name
|
||||
cfh/split-path
|
||||
count)
|
||||
|
||||
max-path-items (apply max (map calc-num-props child-heads))
|
||||
|
||||
first-comp-id (->> parent
|
||||
:shapes
|
||||
first
|
||||
(get objects)
|
||||
:component-id)
|
||||
|
||||
data (pcb/get-library-data changes)
|
||||
variant-properties (get-in data [:components first-comp-id :variant-properties])
|
||||
num-props (count variant-properties)
|
||||
num-new-props (if (< max-path-items num-props)
|
||||
0
|
||||
(- max-path-items num-props))
|
||||
|
||||
changes (nth
|
||||
(iterate #(clv/generate-add-new-property % (:id parent)) changes)
|
||||
num-new-props)]
|
||||
(reduce
|
||||
(fn [changes shape]
|
||||
(if (= (:id parent) (:variant-id shape))
|
||||
changes ;; do nothing if we aren't changing the parent
|
||||
(let [base-name (get-base-name shape)
|
||||
|
||||
;; we need to get the updated library data to have access to the current properties
|
||||
data (pcb/get-library-data changes)
|
||||
|
||||
props (clv/path-to-properties
|
||||
base-name
|
||||
(get-in data [:components first-comp-id :variant-properties]))
|
||||
|
||||
variant-name (clv/properties-to-name props)
|
||||
[cpath cname] (cfh/parse-path-name (:name parent))]
|
||||
|
||||
(-> (pcb/update-component changes
|
||||
(:component-id shape)
|
||||
#(assoc % :variant-id (:id parent)
|
||||
:variant-properties props
|
||||
:name cname
|
||||
:path cpath)
|
||||
{:apply-changes-local-library? true})
|
||||
(pcb/update-shapes [(:id shape)]
|
||||
#(assoc % :variant-id (:id parent)
|
||||
:variant-name variant-name
|
||||
:name (:name parent)))))))
|
||||
changes
|
||||
child-heads)))))
|
||||
|
||||
;; Move the shapes
|
||||
(pcb/change-parent parent-id
|
||||
|
||||
@@ -4,25 +4,21 @@
|
||||
[app.common.types.tokens-lib :as ctob]))
|
||||
|
||||
(defn generate-update-active-sets
|
||||
"Copy the active sets from the currently active themes and move them to the hidden token theme and update the theme with `update-hidden-theme-fn`.
|
||||
|
||||
"Copy the active sets from the currently active themes and move them to the hidden token theme and update the theme with `update-theme-fn`.
|
||||
Use this for managing sets active state without having to modify a user created theme (\"no themes selected\" state in the ui)."
|
||||
[changes tokens-lib update-hidden-theme-fn]
|
||||
[changes tokens-lib update-theme-fn]
|
||||
(let [prev-active-token-themes (ctob/get-active-theme-paths tokens-lib)
|
||||
active-token-set-names (ctob/get-active-themes-set-names tokens-lib)
|
||||
|
||||
prev-hidden-token-theme (ctob/get-hidden-theme tokens-lib)
|
||||
hidden-token-theme (-> (or (some-> prev-hidden-token-theme (ctob/set-sets active-token-set-names))
|
||||
(ctob/make-hidden-token-theme :sets active-token-set-names))
|
||||
(update-hidden-theme-fn))
|
||||
prev-hidden-token-theme (ctob/get-hidden-theme tokens-lib)
|
||||
|
||||
changes (-> changes
|
||||
(pcb/update-active-token-themes #{ctob/hidden-token-theme-path} prev-active-token-themes))
|
||||
|
||||
changes (if prev-hidden-token-theme
|
||||
(pcb/update-token-theme changes hidden-token-theme prev-hidden-token-theme)
|
||||
(pcb/add-token-theme changes hidden-token-theme))]
|
||||
changes))
|
||||
hidden-token-theme (-> (some-> prev-hidden-token-theme (ctob/set-sets active-token-set-names))
|
||||
(update-theme-fn))]
|
||||
(-> changes
|
||||
(pcb/update-active-token-themes #{ctob/hidden-token-theme-path} prev-active-token-themes)
|
||||
(pcb/set-token-theme (:group prev-hidden-token-theme)
|
||||
(:name prev-hidden-token-theme)
|
||||
hidden-token-theme))))
|
||||
|
||||
(defn generate-toggle-token-set
|
||||
"Toggle a token set at `set-name` in `tokens-lib` without modifying a user theme."
|
||||
@@ -48,11 +44,11 @@
|
||||
(defn vec-starts-with? [v1 v2]
|
||||
(= (subvec v1 0 (min (count v1) (count v2))) v2))
|
||||
|
||||
(defn calculate-move-token-set-or-set-group
|
||||
(defn- calculate-move-token-set-or-set-group
|
||||
[tokens-lib {:keys [from-index to-index position collapsed-paths]
|
||||
:or {collapsed-paths #{}}}]
|
||||
(let [tree (-> (ctob/get-set-tree tokens-lib)
|
||||
(ctob/walk-sets-tree-seq :walk-children? #(contains? collapsed-paths %)))
|
||||
(ctob/walk-sets-tree-seq :skip-children-pred #(contains? collapsed-paths %)))
|
||||
from (nth tree from-index)
|
||||
to (nth tree to-index)
|
||||
before (case position
|
||||
|
||||
160
common/src/app/common/logic/variants.cljc
Normal file
160
common/src/app/common/logic/variants.cljc
Normal file
@@ -0,0 +1,160 @@
|
||||
;; 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.logic.variants
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.files.changes-builder :as pcb]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.types.components-list :as ctcl]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
|
||||
|
||||
(def property-prefix "Property")
|
||||
(def property-regex (re-pattern (str property-prefix "(\\d+)")))
|
||||
(def value-prefix "Value")
|
||||
|
||||
(defn find-related-components
|
||||
"Find a list of the components thet belongs to this variant-id"
|
||||
[data objects variant-id]
|
||||
(->> (dm/get-in objects [variant-id :shapes])
|
||||
(map #(dm/get-in objects [% :component-id]))
|
||||
(map #(ctcl/get-component data % true))
|
||||
reverse))
|
||||
|
||||
|
||||
(defn properties-to-name
|
||||
"Transform the properties into a name, with the values separated by comma"
|
||||
[properties]
|
||||
(->> properties
|
||||
(map :value)
|
||||
(remove str/empty?)
|
||||
(str/join ", ")))
|
||||
|
||||
|
||||
(defn next-property-number
|
||||
"Returns the next property number, to avoid duplicates on the property names"
|
||||
[properties]
|
||||
(let [numbers (keep
|
||||
#(some->> (:name %) (re-find property-regex) second d/parse-integer)
|
||||
properties)
|
||||
max-num (if (seq numbers)
|
||||
(apply max numbers)
|
||||
0)]
|
||||
(inc (max max-num (count properties)))))
|
||||
|
||||
|
||||
(defn path-to-properties
|
||||
"From a list of properties and a name with path, assign each token of the
|
||||
path as value of a different property"
|
||||
[path properties]
|
||||
(let [next-prop-num (next-property-number properties)
|
||||
cpath (cfh/split-path path)
|
||||
assigned (mapv #(assoc % :value (nth cpath %2 "")) properties (range))
|
||||
remaining (drop (count properties) cpath)
|
||||
new-properties (map-indexed (fn [i v] {:name (str property-prefix (+ next-prop-num i))
|
||||
:value v}) remaining)]
|
||||
(into assigned new-properties)))
|
||||
|
||||
(defn- dashes-to-end
|
||||
[property-values]
|
||||
(let [dashes (if (some #(= % "--") property-values) ["--"] [])]
|
||||
(concat (remove #(= % "--") property-values) dashes)))
|
||||
|
||||
|
||||
(defn extract-properties-values
|
||||
[data objects variant-id]
|
||||
(->> (find-related-components data objects variant-id)
|
||||
(mapcat :variant-properties)
|
||||
(group-by :name)
|
||||
(map (fn [[k v]]
|
||||
{:name k
|
||||
:value (->> v
|
||||
(map #(if (str/empty? (:value %)) "--" (:value %)))
|
||||
distinct
|
||||
dashes-to-end)}))))
|
||||
|
||||
|
||||
(defn generate-update-property-name
|
||||
[changes variant-id pos new-name]
|
||||
(let [data (pcb/get-library-data changes)
|
||||
objects (pcb/get-objects changes)
|
||||
related-components (find-related-components data objects variant-id)]
|
||||
(reduce (fn [changes component]
|
||||
(pcb/update-component
|
||||
changes (:id component)
|
||||
#(assoc-in % [:variant-properties pos :name] new-name)
|
||||
{:apply-changes-local-library? true}))
|
||||
changes
|
||||
related-components)))
|
||||
|
||||
|
||||
(defn generate-remove-property
|
||||
[changes variant-id pos]
|
||||
(let [data (pcb/get-library-data changes)
|
||||
objects (pcb/get-objects changes)
|
||||
related-components (find-related-components data objects variant-id)]
|
||||
(reduce (fn [changes component]
|
||||
(let [props (:variant-properties component)
|
||||
props (d/remove-at-index props pos)
|
||||
main-id (:main-instance-id component)
|
||||
name (properties-to-name props)]
|
||||
(-> changes
|
||||
(pcb/update-component (:id component) #(assoc % :variant-properties props)
|
||||
{:apply-changes-local-library? true})
|
||||
(pcb/update-shapes [main-id] #(assoc % :variant-name name)))))
|
||||
changes
|
||||
related-components)))
|
||||
|
||||
|
||||
(defn generate-update-property-value
|
||||
[changes component-id pos value]
|
||||
(let [data (pcb/get-library-data changes)
|
||||
component (ctcl/get-component data component-id true)
|
||||
main-id (:main-instance-id component)
|
||||
name (-> (:variant-properties component)
|
||||
(update pos assoc :value value)
|
||||
properties-to-name)]
|
||||
(-> changes
|
||||
(pcb/update-component component-id #(assoc-in % [:variant-properties pos :value] value)
|
||||
{:apply-changes-local-library? true})
|
||||
(pcb/update-shapes [main-id] #(assoc % :variant-name name)))))
|
||||
|
||||
|
||||
(defn generate-add-new-property
|
||||
[changes variant-id & {:keys [fill-values?]}]
|
||||
(let [data (pcb/get-library-data changes)
|
||||
objects (pcb/get-objects changes)
|
||||
related-components (find-related-components data objects variant-id)
|
||||
|
||||
props (-> related-components first :variant-properties)
|
||||
next-prop-num (next-property-number props)
|
||||
property-name (str property-prefix next-prop-num)
|
||||
|
||||
[_ changes]
|
||||
(reduce (fn [[num changes] component]
|
||||
(let [main-id (:main-instance-id component)
|
||||
|
||||
update-props #(-> (d/nilv % [])
|
||||
(conj {:name property-name
|
||||
:value (if fill-values? (str value-prefix num) "")}))
|
||||
|
||||
update-name #(if fill-values?
|
||||
(if (str/empty? %)
|
||||
(str value-prefix num)
|
||||
(str % ", " value-prefix num))
|
||||
%)]
|
||||
[(inc num)
|
||||
(-> changes
|
||||
(pcb/update-component (:id component)
|
||||
#(update % :variant-properties update-props)
|
||||
{:apply-changes-local-library? true})
|
||||
(pcb/update-shapes [main-id] #(update % :variant-name update-name)))]))
|
||||
[1 changes]
|
||||
related-components)]
|
||||
changes))
|
||||
|
||||
@@ -57,6 +57,14 @@
|
||||
:main-instance-page (:id page)
|
||||
:shapes updated-shapes))))))))
|
||||
|
||||
(defn update-component
|
||||
[file component-label & {:keys [] :as params}]
|
||||
(let [component-id (thi/id component-label)]
|
||||
(ctf/update-file-data
|
||||
file
|
||||
(fn [file-data]
|
||||
(ctkl/update-component file-data component-id #(merge % params))))))
|
||||
|
||||
(defn get-component
|
||||
[file label & {:keys [include-deleted?] :or {include-deleted? false}}]
|
||||
(ctkl/get-component (:data file) (thi/id label) include-deleted?))
|
||||
|
||||
@@ -35,6 +35,11 @@
|
||||
(ctob/get-set set-name)
|
||||
(ctob/get-token token-name)))))
|
||||
|
||||
(defn token-data-eq?
|
||||
"Compare token data without comparing modified timestamp"
|
||||
[t1 t2]
|
||||
(= (dissoc t1 :modified-at) (dissoc t2 :modified-at)))
|
||||
|
||||
(defn- set-stroke-width
|
||||
[shape stroke-width]
|
||||
(let [strokes (if (seq (:strokes shape))
|
||||
|
||||
25
common/src/app/common/test_helpers/variants.cljc
Normal file
25
common/src/app/common/test_helpers/variants.cljc
Normal file
@@ -0,0 +1,25 @@
|
||||
;; 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.test-helpers.variants
|
||||
(:require
|
||||
[app.common.test-helpers.components :as thc]
|
||||
[app.common.test-helpers.ids-map :as thi]
|
||||
[app.common.test-helpers.shapes :as ths]))
|
||||
|
||||
(defn add-variant
|
||||
[file variant-label component1-label root1-label component2-label root2-label
|
||||
& {:keys []}]
|
||||
(let [file (ths/add-sample-shape file variant-label :type :frame :is-variant-container true)
|
||||
variant-id (thi/id variant-label)]
|
||||
|
||||
(-> file
|
||||
(ths/add-sample-shape root2-label :type :frame :parent-label variant-label :variant-id variant-id :variant-name "Value2")
|
||||
(ths/add-sample-shape root1-label :type :frame :parent-label variant-label :variant-id variant-id :variant-name "Value1")
|
||||
(thc/make-component component1-label root1-label)
|
||||
(thc/update-component component1-label {:variant-id variant-id :variant-properties [{:name "Property1" :value "Value1"}]})
|
||||
(thc/make-component component2-label root2-label)
|
||||
(thc/update-component component2-label {:variant-id variant-id :variant-properties [{:name "Property1" :value "Value1"}]}))))
|
||||
@@ -118,10 +118,10 @@
|
||||
(def valid-color?
|
||||
(sm/lazy-validator schema:color))
|
||||
|
||||
(def check-color!
|
||||
(def check-color
|
||||
(sm/check-fn schema:color :hint "expected valid color struct"))
|
||||
|
||||
(def check-recent-color!
|
||||
(def check-recent-color
|
||||
(sm/check-fn schema:recent-color))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
@@ -215,6 +215,19 @@
|
||||
(and (= shape-id (:main-instance-id component))
|
||||
(= page-id (:main-instance-page component))))
|
||||
|
||||
|
||||
(defn is-variant?
|
||||
"Check if this shape or component is a variant component"
|
||||
[item]
|
||||
(some? (:variant-id item)))
|
||||
|
||||
|
||||
(defn is-variant-container?
|
||||
"Check if this shape is a variant container"
|
||||
[shape]
|
||||
(:is-variant-container shape))
|
||||
|
||||
|
||||
(defn set-touched-group
|
||||
[touched group]
|
||||
(when group
|
||||
@@ -320,6 +333,8 @@
|
||||
(let [parent (get objects (:parent-id shape))]
|
||||
;; We don't want to change the structure of component copies
|
||||
(and (not (in-component-copy-not-head? shape))
|
||||
;; We don't want to duplicate variants
|
||||
(not (is-variant? shape))
|
||||
;; Non instance, non copy. We allow
|
||||
(or (not (instance-head? shape))
|
||||
(not (in-component-copy? parent))))))
|
||||
|
||||
@@ -34,12 +34,14 @@
|
||||
(assoc component :modified-at (dt/now)))
|
||||
|
||||
(defn add-component
|
||||
[fdata {:keys [id name path main-instance-id main-instance-page shapes annotation]}]
|
||||
[fdata {:keys [id name path main-instance-id main-instance-page shapes annotation variant-id variant-properties]}]
|
||||
(let [components-v2 (dm/get-in fdata [:options :components-v2])
|
||||
fdata (update fdata :components assoc id (touch {:id id :name name :path path}))]
|
||||
(if components-v2
|
||||
(cond-> (update-in fdata [:components id] assoc :main-instance-id main-instance-id :main-instance-page main-instance-page)
|
||||
annotation (update-in [:components id] assoc :annotation annotation))
|
||||
annotation (update-in [:components id] assoc :annotation annotation)
|
||||
variant-id (update-in [:components id] assoc :variant-id variant-id)
|
||||
variant-properties (update-in [:components id] assoc :variant-properties variant-properties))
|
||||
|
||||
(let [wrap-object-fn cfeat/*wrap-with-objects-map-fn*]
|
||||
(assoc-in fdata [:components id :objects]
|
||||
@@ -48,7 +50,7 @@
|
||||
(wrap-object-fn)))))))
|
||||
|
||||
(defn mod-component
|
||||
[file-data {:keys [id name path main-instance-id main-instance-page objects annotation modified-at]}]
|
||||
[file-data {:keys [id name path main-instance-id main-instance-page objects annotation variant-id variant-properties modified-at]}]
|
||||
(let [wrap-objects-fn cfeat/*wrap-with-objects-map-fn*]
|
||||
(d/update-in-when file-data [:components id]
|
||||
(fn [component]
|
||||
@@ -76,10 +78,22 @@
|
||||
(assoc :annotation annotation)
|
||||
|
||||
(nil? annotation)
|
||||
(dissoc :annotation))
|
||||
(dissoc :annotation)
|
||||
|
||||
(some? variant-id)
|
||||
(assoc :variant-id variant-id)
|
||||
|
||||
(nil? variant-id)
|
||||
(dissoc :variant-id)
|
||||
|
||||
(some? variant-properties)
|
||||
(assoc :variant-properties variant-properties)
|
||||
|
||||
(nil? variant-properties)
|
||||
(dissoc :variant-properties))
|
||||
diff (set/difference
|
||||
(ctk/diff-components component new-comp)
|
||||
#{:annotation :modified-at})] ;; The set of properties that doesn't mark a component as touched
|
||||
#{:annotation :modified-at :variant-id :variant-properties})] ;; The set of properties that doesn't mark a component as touched
|
||||
|
||||
(if (empty? diff)
|
||||
new-comp
|
||||
|
||||
@@ -406,7 +406,7 @@
|
||||
(cond-> new-shape
|
||||
:always
|
||||
(-> (gsh/move delta)
|
||||
(dissoc :touched))
|
||||
(dissoc :touched :variant-id :variant-name))
|
||||
|
||||
(and main-instance? root?)
|
||||
(assoc :main-instance true)
|
||||
@@ -501,7 +501,12 @@
|
||||
(defn- invalid-structure-for-component?
|
||||
"Check if the structure generated nesting children in parent is invalid in terms of nested components"
|
||||
[objects parent children pasting? libraries]
|
||||
(let [; When we are pasting, the main shapes will be pasted as copies, unless the
|
||||
(let [; If the original shapes had been cutted, and we are pasting them now, they aren't
|
||||
; in objects. We can add them to locate later
|
||||
objects (merge objects
|
||||
(into {} (map (juxt :id identity) children)))
|
||||
|
||||
; When we are pasting, the main shapes will be pasted as copies, unless the
|
||||
; original component doesn't exist or is deleted. So for this function purposes, they
|
||||
; are removed from the list
|
||||
remove? (fn [shape]
|
||||
@@ -535,11 +540,17 @@
|
||||
(letfn [(get-frame [parent-id]
|
||||
(if (cfh/frame-shape? objects parent-id) parent-id (get-in objects [parent-id :frame-id])))]
|
||||
(let [parent (get objects parent-id)
|
||||
;; We can always move the children to the parent they already have.
|
||||
;; We can always move the children to the parent they already have.
|
||||
;; But if we are pasting, those are new items, so it is considered a change
|
||||
no-changes?
|
||||
(->> children (every? #(= parent-id (:parent-id %))))]
|
||||
;; In case no-changes is true we must ensure we are copy pasting the children in the same position
|
||||
(if (or (and no-changes? (not pasting?)) (not (invalid-structure-for-component? objects parent children pasting? libraries)))
|
||||
(and (->> children (every? #(= parent-id (:parent-id %))))
|
||||
(not pasting?))
|
||||
all-main?
|
||||
(->> children (every? #(ctk/main-instance? %)))]
|
||||
(if (or no-changes?
|
||||
(and (not (invalid-structure-for-component? objects parent children pasting? libraries))
|
||||
;; If we are moving into a variant-container, all the items should be main
|
||||
(or all-main? (not (ctk/is-variant-container? parent)))))
|
||||
[parent-id (get-frame parent-id)]
|
||||
(recur (:parent-id parent) objects children pasting? libraries))))))
|
||||
|
||||
|
||||
@@ -148,7 +148,7 @@
|
||||
|
||||
(sm/register! ::stroke schema:stroke)
|
||||
|
||||
(def check-stroke!
|
||||
(def check-stroke
|
||||
(sm/check-fn schema:stroke))
|
||||
|
||||
(def schema:shape-base-attrs
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
(:require
|
||||
[app.common.schema :as sm]))
|
||||
|
||||
(def types #{:png :jpeg :svg :pdf})
|
||||
(def types #{:png :jpeg :webp :svg :pdf})
|
||||
|
||||
(def schema:export
|
||||
[:map {:title "ShapeExport"}
|
||||
|
||||
@@ -1643,15 +1643,10 @@
|
||||
untouched as possible"
|
||||
[target-cells source-cells omit-touched?]
|
||||
(if omit-touched?
|
||||
(letfn [(get-data [cells id]
|
||||
(dissoc (get cells id) :row :column :row-span :column-span))
|
||||
|
||||
(merge-cells [source-cell target-cell]
|
||||
(letfn [(merge-cells [source-cell target-cell]
|
||||
(-> source-cell
|
||||
(d/patch-object
|
||||
(dissoc target-cell :shapes :row :column :row-span :column-span))
|
||||
(cond-> (d/not-empty? (:shapes target-cell))
|
||||
(assoc :shapes (:shapes target-cell)))))]
|
||||
(dissoc target-cell :row :column :row-span :column-span))))]
|
||||
(let [deleted-cells
|
||||
(into #{}
|
||||
(filter #(not (contains? source-cells %)))
|
||||
@@ -1659,10 +1654,7 @@
|
||||
|
||||
touched-cells
|
||||
(into #{}
|
||||
(filter #(and
|
||||
(not (contains? deleted-cells %))
|
||||
(not= (get-data source-cells %)
|
||||
(get-data target-cells %))))
|
||||
(filter #(not (contains? deleted-cells %)))
|
||||
(keys target-cells))]
|
||||
|
||||
(->> touched-cells
|
||||
@@ -1672,3 +1664,12 @@
|
||||
(d/update-when id merge-cells (get target-cells id))))
|
||||
source-cells))))
|
||||
source-cells))
|
||||
|
||||
(defn toggle-fix-if-auto
|
||||
"Changes the sizing to fix if it's fill"
|
||||
[shape]
|
||||
(cond-> shape
|
||||
(= (:layout-item-h-sizing shape) :fill)
|
||||
(assoc :layout-item-h-sizing :fix)
|
||||
(= (:layout-item-v-sizing shape) :fill)
|
||||
(assoc :layout-item-v-sizing :fix)))
|
||||
|
||||
@@ -28,5 +28,5 @@
|
||||
|
||||
(sm/register! ::shadow schema:shadow)
|
||||
|
||||
(def check-shadow!
|
||||
(def check-shadow
|
||||
(sm/check-fn schema:shadow))
|
||||
|
||||
@@ -129,6 +129,10 @@
|
||||
[:p2 {:optional true} token-name-ref]
|
||||
[:p3 {:optional true} token-name-ref]
|
||||
[:p4 {:optional true} token-name-ref]
|
||||
[:m1 {:optional true} token-name-ref]
|
||||
[:m2 {:optional true} token-name-ref]
|
||||
[:m3 {:optional true} token-name-ref]
|
||||
[:m4 {:optional true} token-name-ref]
|
||||
[:x {:optional true} token-name-ref]
|
||||
[:y {:optional true} token-name-ref]])
|
||||
|
||||
@@ -184,6 +188,8 @@
|
||||
(cond
|
||||
(some #{:stroke-color} changed-sub-attr) #{:stroke-color}
|
||||
(some #{:stroke-width} changed-sub-attr) #{:stroke-width})
|
||||
(and (= :layout-padding shape-attr) (seq changed-sub-attr)) changed-sub-attr
|
||||
(and (= :layout-item-margin shape-attr) (seq changed-sub-attr)) changed-sub-attr
|
||||
(border-radius-keys shape-attr) #{shape-attr}
|
||||
(sizing-keys shape-attr) #{shape-attr}
|
||||
(opacity-keys shape-attr) #{shape-attr}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
[:group :string]
|
||||
[:description [:maybe :string]]
|
||||
[:is-source :boolean]
|
||||
[:id :string]
|
||||
[:modified-at {:optional true} ::sm/inst]
|
||||
[:sets :any]])
|
||||
|
||||
@@ -24,4 +25,4 @@
|
||||
[:name :string]
|
||||
[:description {:optional true} [:maybe :string]]
|
||||
[:modified-at {:optional true} ::sm/inst]
|
||||
[:tokens :any]])
|
||||
[:tokens {:optional true} :any]])
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -95,6 +95,11 @@
|
||||
(impl/getUnsignedParts (.-uuid ^UUID this))))
|
||||
|
||||
|
||||
#?(:cljs
|
||||
(defn from-unsigned-parts
|
||||
[a b c d]
|
||||
(uuid (impl/fromUnsignedParts a b c d))))
|
||||
|
||||
#?(:cljs
|
||||
(defn get-u32
|
||||
"A cached variant of get-unsigned-parts"
|
||||
|
||||
@@ -51,9 +51,13 @@
|
||||
(t/is (= [0 0 0] (colors/hex->rgb "#kkk")))
|
||||
(t/is (= [1 2 3] (colors/hex->rgb "#010203"))))
|
||||
|
||||
#?(:cljs
|
||||
(t/deftest format-hsla
|
||||
(t/is (= "210, 50%, 1%, 1" (colors/format-hsla [210.0 0.5 0.00784313725490196 1])))))
|
||||
(t/deftest format-hsla
|
||||
(t/is (= "210, 50%, 0.78%, 1" (colors/format-hsla [210.0 0.5 0.00784313725490196 1])))
|
||||
(t/is (= "220, 5%, 30%, 0.8" (colors/format-hsla [220.0 0.05 0.3 0.8]))))
|
||||
|
||||
(t/deftest format-rgba
|
||||
(t/is (= "210, 199, 12, 0.08" (colors/format-rgba [210 199 12 0.08])))
|
||||
(t/is (= "210, 199, 12, 1" (colors/format-rgba [210 199 12 1]))))
|
||||
|
||||
(t/deftest rgb-to-hsl
|
||||
(t/is (= [210.0 0.5 0.00784313725490196] (colors/rgb->hsl [1 2 3])))
|
||||
|
||||
38
common/test/common_tests/files/helpers_test.cljc
Normal file
38
common/test/common_tests/files/helpers_test.cljc
Normal file
@@ -0,0 +1,38 @@
|
||||
;; 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.helpers-test
|
||||
(:require
|
||||
[app.common.files.helpers :as cfh]
|
||||
[clojure.test :as t]))
|
||||
|
||||
(t/deftest test-generate-unique-name
|
||||
(t/testing "Test unique name generation"
|
||||
(let [suffix-fn #(str "-copy-" %)]
|
||||
(t/is (cfh/generate-unique-name "base-name"
|
||||
#{"base-name" "base-name-copy-1"}
|
||||
:suffix-fn suffix-fn)
|
||||
"base-name-copy-2")
|
||||
(t/is (cfh/generate-unique-name "base-name"
|
||||
#{"base-name-copy-2"}
|
||||
:suffix-fn suffix-fn)
|
||||
"base-name-copy-1")
|
||||
(t/is (cfh/generate-unique-name "base-name"
|
||||
#{"base-namec-copy"}
|
||||
:suffix-fn suffix-fn)
|
||||
"base-name-copy-1")
|
||||
(t/is (cfh/generate-unique-name "base-name"
|
||||
#{"base-name"}
|
||||
:suffix-fn suffix-fn)
|
||||
"base-name-copy-1")))
|
||||
|
||||
(t/testing "Test unique name generation with immidate suffix and default suffix-fn"
|
||||
(t/is (cfh/generate-unique-name "base-name" #{} :immediate-suffix? true)
|
||||
"base-name 1")
|
||||
(t/is (cfh/generate-unique-name "base-name"
|
||||
#{"base-name 1" "base-name 2"}
|
||||
:immediate-suffix? true)
|
||||
"base-name 3")))
|
||||
@@ -20,6 +20,7 @@
|
||||
[app.common.types.component :as ctk]
|
||||
[app.common.types.components-list :as ctkl]
|
||||
[app.common.types.shape-tree :as ctst]
|
||||
[app.common.uuid :as uuid]
|
||||
[clojure.test :as t]))
|
||||
|
||||
(t/use-fixtures :each thi/test-fixture)
|
||||
@@ -285,10 +286,12 @@
|
||||
component (thc/get-component file :component1)
|
||||
|
||||
;; ==== Action
|
||||
changes (cll/generate-duplicate-component (pcb/empty-changes)
|
||||
file
|
||||
(:id component)
|
||||
true)
|
||||
[_ changes]
|
||||
(cll/generate-duplicate-component (pcb/empty-changes)
|
||||
file
|
||||
(:id component)
|
||||
(uuid/next)
|
||||
true)
|
||||
|
||||
file' (thf/apply-changes file changes)
|
||||
|
||||
|
||||
@@ -61,10 +61,10 @@
|
||||
blue1 (ths/get-shape file :blue1)
|
||||
|
||||
;; ==== Action
|
||||
changes (cls/generate-relocate (pcb/empty-changes nil)
|
||||
(:objects page)
|
||||
changes (cls/generate-relocate (-> (pcb/empty-changes nil)
|
||||
(pcb/with-page-id (:id page))
|
||||
(pcb/with-objects (:objects page)))
|
||||
uuid/zero ;; parent-id
|
||||
(:id page) ;; page-id
|
||||
0 ;; to-index
|
||||
#{(:id blue1)}) ;; ids
|
||||
|
||||
@@ -91,10 +91,10 @@
|
||||
|
||||
|
||||
;; ==== Action
|
||||
changes (cls/generate-relocate (pcb/empty-changes nil)
|
||||
(:objects page)
|
||||
changes (cls/generate-relocate (-> (pcb/empty-changes nil)
|
||||
(pcb/with-page-id (:id page))
|
||||
(pcb/with-objects (:objects page)))
|
||||
(:id b2) ;; parent-id
|
||||
(:id page) ;; page-id
|
||||
0 ;; to-index
|
||||
#{(:id blue1)}) ;; ids
|
||||
|
||||
@@ -121,10 +121,10 @@
|
||||
|
||||
;; ==== Action
|
||||
;; Move blue1 into yellow
|
||||
changes (cls/generate-relocate (pcb/empty-changes nil)
|
||||
(:objects page)
|
||||
changes (cls/generate-relocate (-> (pcb/empty-changes nil)
|
||||
(pcb/with-page-id (:id page))
|
||||
(pcb/with-objects (:objects page)))
|
||||
(:id yellow) ;; parent-id
|
||||
(:id page) ;; page-id
|
||||
0 ;; to-index
|
||||
#{(:id blue1)}) ;; ids
|
||||
|
||||
@@ -134,10 +134,10 @@
|
||||
yellow' (ths/get-shape file' :frame-yellow)
|
||||
|
||||
;; Move yellow into root
|
||||
changes' (cls/generate-relocate (pcb/empty-changes nil)
|
||||
(:objects page')
|
||||
changes' (cls/generate-relocate (-> (pcb/empty-changes nil)
|
||||
(pcb/with-page-id (:id page'))
|
||||
(pcb/with-objects (:objects page')))
|
||||
uuid/zero ;; parent-id
|
||||
(:id page') ;; page-id
|
||||
0 ;; to-index
|
||||
#{(:id yellow')}) ;; ids
|
||||
|
||||
@@ -164,10 +164,10 @@
|
||||
|
||||
;; ==== Action
|
||||
;; Move blue1 into yellow
|
||||
changes (cls/generate-relocate (pcb/empty-changes nil)
|
||||
(:objects page)
|
||||
changes (cls/generate-relocate (-> (pcb/empty-changes nil)
|
||||
(pcb/with-page-id (:id page))
|
||||
(pcb/with-objects (:objects page)))
|
||||
(:id yellow) ;; parent-id
|
||||
(:id page) ;; page-id
|
||||
0 ;; to-index
|
||||
#{(:id blue1)}) ;; ids
|
||||
|
||||
@@ -178,10 +178,10 @@
|
||||
b2' (ths/get-shape file' :frame-b2)
|
||||
|
||||
;; Move yellow into b2
|
||||
changes' (cls/generate-relocate (pcb/empty-changes nil)
|
||||
(:objects page')
|
||||
changes' (cls/generate-relocate (-> (pcb/empty-changes nil)
|
||||
(pcb/with-page-id (:id page'))
|
||||
(pcb/with-objects (:objects page')))
|
||||
(:id b2') ;; parent-id
|
||||
(:id page') ;; page-id
|
||||
0 ;; to-index
|
||||
#{(:id yellow')}) ;; ids
|
||||
|
||||
@@ -254,10 +254,10 @@
|
||||
|
||||
;; ==== Action
|
||||
;; Move blue1 into yellow
|
||||
changes (cls/generate-relocate (pcb/empty-changes nil)
|
||||
(:objects page)
|
||||
changes (cls/generate-relocate (-> (pcb/empty-changes nil)
|
||||
(pcb/with-page-id (:id page))
|
||||
(pcb/with-objects (:objects page)))
|
||||
(:id yellow) ;; parent-id
|
||||
(:id page) ;; page-id
|
||||
0 ;; to-index
|
||||
#{(:id blue1)}) ;; ids
|
||||
|
||||
@@ -308,10 +308,10 @@
|
||||
blue1 (ths/get-shape file :blue1)
|
||||
|
||||
;; ==== Action
|
||||
changes (cls/generate-relocate (pcb/empty-changes nil)
|
||||
(:objects page)
|
||||
changes (cls/generate-relocate (-> (pcb/empty-changes nil)
|
||||
(pcb/with-page-id (:id page))
|
||||
(pcb/with-objects (:objects page)))
|
||||
(:parent-id blue1) ;; parent-id
|
||||
(:id page) ;; page-id
|
||||
2 ;; to-index
|
||||
#{(:id blue1)}) ;; ids
|
||||
|
||||
@@ -338,10 +338,10 @@
|
||||
|
||||
;; ==== Action
|
||||
;; Move blue1 into yellow
|
||||
changes (cls/generate-relocate (pcb/empty-changes nil)
|
||||
(:objects page)
|
||||
changes (cls/generate-relocate (-> (pcb/empty-changes nil)
|
||||
(pcb/with-page-id (:id page))
|
||||
(pcb/with-objects (:objects page)))
|
||||
(:id yellow) ;; parent-id
|
||||
(:id page) ;; page-id
|
||||
0 ;; to-index
|
||||
#{(:id blue1)}) ;; ids
|
||||
|
||||
@@ -351,10 +351,10 @@
|
||||
page' (thf/current-page file')
|
||||
blue1' (ths/get-shape file' :blue1)
|
||||
b1' (ths/get-shape file' :frame-b1)
|
||||
changes' (cls/generate-relocate (pcb/empty-changes nil)
|
||||
(:objects page')
|
||||
changes' (cls/generate-relocate (-> (pcb/empty-changes nil)
|
||||
(pcb/with-page-id (:id page'))
|
||||
(pcb/with-objects (:objects page')))
|
||||
(:id b1') ;; parent-id
|
||||
(:id page) ;; page-id
|
||||
0 ;; to-index
|
||||
#{(:id blue1')}) ;; ids
|
||||
|
||||
@@ -382,10 +382,10 @@
|
||||
|
||||
;; ==== Action
|
||||
;; Relocate blue1 into yellow
|
||||
changes (cls/generate-relocate (pcb/empty-changes nil)
|
||||
(:objects page)
|
||||
changes (cls/generate-relocate (-> (pcb/empty-changes nil)
|
||||
(pcb/with-page-id (:id page))
|
||||
(pcb/with-objects (:objects page)))
|
||||
(:id yellow) ;; parent-id
|
||||
(:id page) ;; page-id
|
||||
0 ;; to-index
|
||||
#{(:id blue1)}) ;; ids
|
||||
|
||||
@@ -396,10 +396,10 @@
|
||||
page' (thf/current-page file')
|
||||
blue1' (ths/get-shape file' :blue1)
|
||||
b1' (ths/get-shape file' :frame-b1)
|
||||
changes' (cls/generate-relocate (pcb/empty-changes nil)
|
||||
(:objects page')
|
||||
changes' (cls/generate-relocate (-> (pcb/empty-changes nil)
|
||||
(pcb/with-page-id (:id page'))
|
||||
(pcb/with-objects (:objects page')))
|
||||
(:id b1') ;; parent-id
|
||||
(:id page') ;; page-id
|
||||
0 ;; to-index
|
||||
#{(:id blue1')}) ;; ids
|
||||
|
||||
@@ -428,10 +428,10 @@
|
||||
green-copy (ths/get-shape file :green-copy)
|
||||
|
||||
;; ==== Action
|
||||
changes (cls/generate-relocate (pcb/empty-changes nil)
|
||||
(:objects page)
|
||||
changes (cls/generate-relocate (-> (pcb/empty-changes nil)
|
||||
(pcb/with-page-id (:id page))
|
||||
(pcb/with-objects (:objects page)))
|
||||
uuid/zero ;; parent-id
|
||||
(:id page) ;; page-id
|
||||
0 ;; to-index
|
||||
#{(:id green-copy)}) ;; ids
|
||||
|
||||
@@ -459,10 +459,10 @@
|
||||
b2 (ths/get-shape file :frame-b2)
|
||||
|
||||
;; ==== Action
|
||||
changes (cls/generate-relocate (pcb/empty-changes nil)
|
||||
(:objects page)
|
||||
changes (cls/generate-relocate (-> (pcb/empty-changes nil)
|
||||
(pcb/with-page-id (:id page))
|
||||
(pcb/with-objects (:objects page)))
|
||||
(:id b2) ;; parent-id
|
||||
(:id page) ;; page-id
|
||||
0 ;; to-index
|
||||
#{(:id green-copy)}) ;; ids
|
||||
|
||||
|
||||
@@ -136,10 +136,10 @@
|
||||
|
||||
;; IMPORTANT: as modifying copies structure is now forbidden, this action
|
||||
;; will not have any effect, and so the parent shape won't also be touched.
|
||||
changes (cls/generate-relocate (pcb/empty-changes)
|
||||
(:objects page)
|
||||
changes (cls/generate-relocate (-> (pcb/empty-changes nil)
|
||||
(pcb/with-page-id (:id page))
|
||||
(pcb/with-objects (:objects page)))
|
||||
(thi/id :copy-root) ; parent-id
|
||||
(:id page) ; page-id
|
||||
0 ; to-index
|
||||
#{(thi/id :free-shape)}) ; ids
|
||||
|
||||
@@ -231,10 +231,10 @@
|
||||
|
||||
;; IMPORTANT: as modifying copies structure is now forbidden, this action
|
||||
;; will not have any effect, and so the parent shape won't also be touched.
|
||||
changes (cls/generate-relocate (pcb/empty-changes)
|
||||
(:objects page)
|
||||
changes (cls/generate-relocate (-> (pcb/empty-changes nil)
|
||||
(pcb/with-page-id (:id page))
|
||||
(pcb/with-objects (:objects page)))
|
||||
(thi/id :copy-root) ; parent-id
|
||||
(:id page) ; page-id
|
||||
2 ; to-index
|
||||
#{(:id copy-child1)}) ; ids
|
||||
|
||||
|
||||
@@ -195,10 +195,10 @@
|
||||
page (thf/current-page file)
|
||||
|
||||
;; ==== Action
|
||||
changes1 (cls/generate-relocate (pcb/empty-changes)
|
||||
(:objects page)
|
||||
changes1 (cls/generate-relocate (-> (pcb/empty-changes nil)
|
||||
(pcb/with-page-id (:id page))
|
||||
(pcb/with-objects (:objects page)))
|
||||
(thi/id :main-root) ; parent-id
|
||||
(:id page) ; page-id
|
||||
0 ; to-index
|
||||
#{(thi/id :free-shape)}) ; ids
|
||||
|
||||
@@ -292,10 +292,10 @@
|
||||
main-child1 (ths/get-shape file :main-child1)
|
||||
|
||||
;; ==== Action
|
||||
changes1 (cls/generate-relocate (pcb/empty-changes)
|
||||
(:objects page)
|
||||
changes1 (cls/generate-relocate (-> (pcb/empty-changes nil)
|
||||
(pcb/with-page-id (:id page))
|
||||
(pcb/with-objects (:objects page)))
|
||||
(thi/id :main-root) ; parent-id
|
||||
(:id page) ; page-id
|
||||
2 ; to-index
|
||||
#{(:id main-child1)}) ; ids
|
||||
|
||||
|
||||
@@ -112,10 +112,10 @@
|
||||
|
||||
;; IMPORTANT: as modifying copies structure is now forbidden, this action
|
||||
;; will not have any effect, and so the parent shape won't also be touched.
|
||||
changes (cls/generate-relocate (pcb/empty-changes)
|
||||
(:objects page)
|
||||
changes (cls/generate-relocate (-> (pcb/empty-changes nil)
|
||||
(pcb/with-page-id (:id page))
|
||||
(pcb/with-objects (:objects page)))
|
||||
(thi/id :copy-root) ; parent-id
|
||||
(:id page) ; page-id
|
||||
0 ; to-index
|
||||
#{(thi/id :free-shape)}) ; ids
|
||||
|
||||
@@ -187,10 +187,10 @@
|
||||
|
||||
;; IMPORTANT: as modifying copies structure is now forbidden, this action
|
||||
;; will not have any effect, and so the parent shape won't also be touched.
|
||||
changes (cls/generate-relocate (pcb/empty-changes)
|
||||
(:objects page)
|
||||
changes (cls/generate-relocate (-> (pcb/empty-changes nil)
|
||||
(pcb/with-page-id (:id page))
|
||||
(pcb/with-objects (:objects page)))
|
||||
(thi/id :copy-root) ; parent-id
|
||||
(:id page) ; page-id
|
||||
2 ; to-index
|
||||
#{(:id copy-child1)}) ; ids
|
||||
|
||||
|
||||
@@ -29,10 +29,10 @@
|
||||
|
||||
;; ==== Action
|
||||
|
||||
changes (cls/generate-relocate (pcb/empty-changes nil)
|
||||
(:objects page)
|
||||
changes (cls/generate-relocate (-> (pcb/empty-changes nil)
|
||||
(pcb/with-page-id (:id page))
|
||||
(pcb/with-objects (:objects page)))
|
||||
(:id frame-parent) ;; parent-id
|
||||
(:id page) ;; page-id
|
||||
0 ;; to-index
|
||||
#{(:id frame-to-move)}) ;; ids
|
||||
|
||||
@@ -61,10 +61,10 @@
|
||||
|
||||
;; ==== Action
|
||||
|
||||
changes (cls/generate-relocate (pcb/empty-changes nil)
|
||||
(:objects page)
|
||||
changes (cls/generate-relocate (-> (pcb/empty-changes nil)
|
||||
(pcb/with-page-id (:id page))
|
||||
(pcb/with-objects (:objects page)))
|
||||
uuid/zero ;; parent-id
|
||||
(:id page) ;; page-id
|
||||
0 ;; to-index
|
||||
#{(:id circle)}) ;; ids
|
||||
|
||||
|
||||
@@ -22,7 +22,9 @@
|
||||
(ctob/add-theme (ctob/make-token-theme :name "theme"
|
||||
:sets #{"foo/bar"}))
|
||||
(ctob/set-active-themes #{"/theme"})))
|
||||
changes (clt/generate-toggle-token-set (pcb/empty-changes) (tht/get-tokens-lib file) "foo/bar")
|
||||
changes (-> (pcb/empty-changes)
|
||||
(pcb/with-library-data (:data file))
|
||||
(clt/generate-toggle-token-set (tht/get-tokens-lib file) "foo/bar"))
|
||||
|
||||
redo (thf/apply-changes file changes)
|
||||
redo-lib (tht/get-tokens-lib redo)
|
||||
@@ -32,7 +34,6 @@
|
||||
(t/is (= #{} (:sets (ctob/get-hidden-theme redo-lib))))
|
||||
|
||||
;; Undo
|
||||
(t/is (nil? (ctob/get-hidden-theme undo-lib)))
|
||||
(t/is (= #{"/theme"} (ctob/get-active-theme-paths undo-lib)))))
|
||||
|
||||
(t/testing "toggling an inactive set will switch to hidden theme without user sets"
|
||||
@@ -41,7 +42,9 @@
|
||||
(ctob/add-theme (ctob/make-token-theme :name "theme"
|
||||
:sets #{"foo/bar"}))
|
||||
(ctob/set-active-themes #{"/theme"})))
|
||||
changes (clt/generate-toggle-token-set (pcb/empty-changes) (tht/get-tokens-lib file) "foo/bar")
|
||||
changes (-> (pcb/empty-changes)
|
||||
(pcb/with-library-data (:data file))
|
||||
(clt/generate-toggle-token-set (tht/get-tokens-lib file) "foo/bar"))
|
||||
|
||||
redo (thf/apply-changes file changes)
|
||||
redo-lib (tht/get-tokens-lib redo)
|
||||
@@ -51,7 +54,6 @@
|
||||
(t/is (= #{} (:sets (ctob/get-hidden-theme redo-lib))))
|
||||
|
||||
;; Undo
|
||||
(t/is (nil? (ctob/get-hidden-theme undo-lib)))
|
||||
(t/is (= #{"/theme"} (ctob/get-active-theme-paths undo-lib)))))
|
||||
|
||||
(t/testing "toggling an set with hidden theme already active will toggle set in hidden theme"
|
||||
@@ -60,7 +62,9 @@
|
||||
(ctob/add-theme (ctob/make-hidden-token-theme))
|
||||
(ctob/set-active-themes #{ctob/hidden-token-theme-path})))
|
||||
|
||||
changes (clt/generate-toggle-token-set-group (pcb/empty-changes) (tht/get-tokens-lib file) ["foo"])
|
||||
changes (-> (pcb/empty-changes)
|
||||
(pcb/with-library-data (:data file))
|
||||
(clt/generate-toggle-token-set-group (tht/get-tokens-lib file) ["foo"]))
|
||||
|
||||
redo (thf/apply-changes file changes)
|
||||
redo-lib (tht/get-tokens-lib redo)
|
||||
@@ -68,10 +72,210 @@
|
||||
undo-lib (tht/get-tokens-lib undo)]
|
||||
(t/is (= (ctob/get-active-theme-paths redo-lib) (ctob/get-active-theme-paths undo-lib)))
|
||||
|
||||
(t/is (= #{"foo/bar"} (:sets (ctob/get-hidden-theme redo-lib))))
|
||||
(t/is (= #{"foo/bar"} (:sets (ctob/get-hidden-theme redo-lib)))))))
|
||||
|
||||
(t/deftest set-token-theme-test
|
||||
(t/testing "delete token theme"
|
||||
(let [theme-name "foo"
|
||||
group "main"
|
||||
file (setup-file #(-> %
|
||||
(ctob/add-theme (ctob/make-token-theme :name theme-name
|
||||
:group group))))
|
||||
changes (-> (pcb/empty-changes)
|
||||
(pcb/with-library-data (:data file))
|
||||
(pcb/set-token-theme group theme-name nil))
|
||||
|
||||
redo (thf/apply-changes file changes)
|
||||
redo-lib (tht/get-tokens-lib redo)
|
||||
undo (thf/apply-undo-changes redo changes)
|
||||
undo-lib (tht/get-tokens-lib undo)]
|
||||
;; Redo
|
||||
(t/is (nil? (ctob/get-theme redo-lib group theme-name)))
|
||||
;; Undo
|
||||
(t/is (some? (ctob/get-theme undo-lib group theme-name)))))
|
||||
|
||||
(t/testing "add token theme"
|
||||
(let [theme-name "foo"
|
||||
group "main"
|
||||
theme (ctob/make-token-theme :name theme-name
|
||||
:group group)
|
||||
file (setup-file identity)
|
||||
changes (-> (pcb/empty-changes)
|
||||
(pcb/with-library-data (:data file))
|
||||
(pcb/set-token-theme group theme-name theme))
|
||||
redo (thf/apply-changes file changes)
|
||||
redo-lib (tht/get-tokens-lib redo)
|
||||
undo (thf/apply-undo-changes redo changes)
|
||||
undo-lib (tht/get-tokens-lib undo)]
|
||||
;; Redo
|
||||
(t/is (some? (ctob/get-theme redo-lib group theme-name)))
|
||||
;; Undo
|
||||
(t/is (nil? (ctob/get-theme undo-lib group theme-name)))))
|
||||
|
||||
(t/testing "update token theme"
|
||||
(let [theme-name "foo"
|
||||
group "main"
|
||||
prev-theme (ctob/make-token-theme :name theme-name
|
||||
:group group)
|
||||
file (setup-file #(ctob/add-theme % prev-theme))
|
||||
new-theme-name "foo1"
|
||||
changes (-> (pcb/empty-changes)
|
||||
(pcb/with-library-data (:data file))
|
||||
(pcb/set-token-theme group new-theme-name prev-theme))
|
||||
redo (thf/apply-changes file changes)
|
||||
redo-lib (tht/get-tokens-lib redo)
|
||||
undo (thf/apply-undo-changes redo changes)
|
||||
undo-lib (tht/get-tokens-lib undo)]
|
||||
;; Redo
|
||||
(t/is (some? (ctob/get-theme redo-lib group theme-name)))
|
||||
(t/is (nil? (ctob/get-theme redo-lib group new-theme-name)))
|
||||
;; Undo
|
||||
(t/is (some? (ctob/get-theme undo-lib group theme-name)))
|
||||
(t/is (nil? (ctob/get-theme undo-lib group new-theme-name)))))
|
||||
|
||||
(t/testing "toggling token theme updates using changes history"
|
||||
(let [theme-name "foo-theme"
|
||||
group "main"
|
||||
set-name "bar-set"
|
||||
token-set (ctob/make-token-set :name set-name)
|
||||
theme (ctob/make-token-theme :name theme-name
|
||||
:group group)
|
||||
file (setup-file #(-> %
|
||||
(ctob/add-theme theme)
|
||||
(ctob/add-set token-set)))
|
||||
theme' (assoc theme :sets #{set-name})
|
||||
changes (-> (pcb/empty-changes)
|
||||
(pcb/with-library-data (:data file))
|
||||
(pcb/set-token-theme group theme-name theme'))
|
||||
changed-file (-> file
|
||||
(thf/apply-changes changes)
|
||||
(thf/apply-undo-changes changes)
|
||||
(thf/apply-changes changes))
|
||||
changed-lib (tht/get-tokens-lib changed-file)]
|
||||
(t/is (= #{set-name}
|
||||
(-> changed-lib (ctob/get-theme group theme-name) :sets))))))
|
||||
|
||||
(t/deftest set-token-test
|
||||
(t/testing "delete token"
|
||||
(let [set-name "foo"
|
||||
token-name "to.delete.color.red"
|
||||
file (setup-file #(-> %
|
||||
(ctob/add-set (ctob/make-token-set :name set-name))
|
||||
(ctob/add-token-in-set set-name (ctob/make-token {:name token-name
|
||||
:value "red"
|
||||
:type :color}))))
|
||||
changes (-> (pcb/empty-changes)
|
||||
(pcb/with-library-data (:data file))
|
||||
(pcb/set-token set-name token-name nil))
|
||||
|
||||
redo (thf/apply-changes file changes)
|
||||
redo-lib (tht/get-tokens-lib redo)
|
||||
undo (thf/apply-undo-changes redo changes)
|
||||
undo-lib (tht/get-tokens-lib undo)]
|
||||
(t/is (nil? (ctob/get-token-in-set redo-lib set-name token-name)))
|
||||
;; Undo
|
||||
(t/is (some? (ctob/get-token-in-set undo-lib set-name token-name)))))
|
||||
|
||||
(t/testing "add token"
|
||||
(let [set-name "foo"
|
||||
token (ctob/make-token {:name "to.add.color.red"
|
||||
:value "red"
|
||||
:type :color})
|
||||
file (setup-file #(-> % (ctob/add-set (ctob/make-token-set :name set-name))))
|
||||
changes (-> (pcb/empty-changes)
|
||||
(pcb/with-library-data (:data file))
|
||||
(pcb/set-token set-name (:name token) token))
|
||||
|
||||
redo (thf/apply-changes file changes)
|
||||
redo-lib (tht/get-tokens-lib redo)
|
||||
undo (thf/apply-undo-changes redo changes)
|
||||
undo-lib (tht/get-tokens-lib undo)]
|
||||
(t/is (= token (ctob/get-token-in-set redo-lib set-name (:name token))))
|
||||
;; Undo
|
||||
(t/is (nil? (ctob/get-token-in-set undo-lib set-name (:name token))))))
|
||||
|
||||
(t/testing "update token"
|
||||
(let [set-name "foo"
|
||||
prev-token (ctob/make-token {:name "to.update.color.red"
|
||||
:value "red"
|
||||
:type :color})
|
||||
token (-> prev-token
|
||||
(assoc :name "color.red.changed")
|
||||
(assoc :value "blue"))
|
||||
file (setup-file #(-> %
|
||||
(ctob/add-set (ctob/make-token-set :name set-name))
|
||||
(ctob/add-token-in-set set-name prev-token)))
|
||||
changes (-> (pcb/empty-changes)
|
||||
(pcb/with-library-data (:data file))
|
||||
(pcb/set-token set-name (:name prev-token) token))
|
||||
|
||||
redo (thf/apply-changes file changes)
|
||||
redo-lib (tht/get-tokens-lib redo)
|
||||
undo (thf/apply-undo-changes redo changes)
|
||||
undo-lib (tht/get-tokens-lib undo)]
|
||||
(t/is (tht/token-data-eq? token (ctob/get-token-in-set redo-lib set-name (:name token))))
|
||||
(t/is (nil? (ctob/get-token-in-set redo-lib set-name (:name prev-token))))
|
||||
;; Undo
|
||||
(t/is (tht/token-data-eq? prev-token (ctob/get-token-in-set undo-lib set-name (:name prev-token))))
|
||||
(t/is (nil? (ctob/get-token-in-set undo-lib set-name (:name token)))))))
|
||||
|
||||
(t/deftest set-token-set-test
|
||||
(t/testing "delete token set"
|
||||
(let [set-name "foo"
|
||||
file (setup-file #(ctob/add-set % (ctob/make-token-set :name set-name)))
|
||||
changes (-> (pcb/empty-changes)
|
||||
(pcb/with-library-data (:data file))
|
||||
(pcb/set-token-set set-name false nil))
|
||||
|
||||
redo (thf/apply-changes file changes)
|
||||
redo-lib (tht/get-tokens-lib redo)
|
||||
undo (thf/apply-undo-changes redo changes)
|
||||
undo-lib (tht/get-tokens-lib undo)]
|
||||
(t/is (not (ctob/set-path-exists? redo-lib [set-name])))
|
||||
;; Undo
|
||||
(t/is (ctob/set-path-exists? undo-lib [set-name]))))
|
||||
|
||||
(t/testing "add token set"
|
||||
(let [set-name "foo"
|
||||
token-set (ctob/make-token-set :name set-name)
|
||||
file (setup-file identity)
|
||||
changes (-> (pcb/empty-changes)
|
||||
(pcb/with-library-data (:data file))
|
||||
(pcb/set-token-set set-name false token-set))
|
||||
|
||||
redo (thf/apply-changes file changes)
|
||||
redo-lib (tht/get-tokens-lib redo)
|
||||
undo (thf/apply-undo-changes redo changes)
|
||||
undo-lib (tht/get-tokens-lib undo)]
|
||||
(t/is (not (ctob/set-path-exists? undo-lib [set-name])))
|
||||
;; Undo
|
||||
(t/is (ctob/set-path-exists? redo-lib [set-name]))))
|
||||
|
||||
(t/testing "update token set"
|
||||
(let [set-name "foo"
|
||||
token-name "bar"
|
||||
token (ctob/make-token {:name token-name
|
||||
:value "red"
|
||||
:type :color})
|
||||
file (setup-file #(-> (ctob/add-set % (ctob/make-token-set :name set-name))
|
||||
(ctob/add-token-in-set set-name token)))
|
||||
prev-token-set (-> file tht/get-tokens-lib :sets first)
|
||||
new-set-name "foo1"
|
||||
changes (-> (pcb/empty-changes)
|
||||
(pcb/with-library-data (:data file))
|
||||
(pcb/set-token-set set-name false (assoc prev-token-set
|
||||
:name new-set-name)))
|
||||
redo (thf/apply-changes file changes)
|
||||
redo-lib (tht/get-tokens-lib redo)
|
||||
undo (thf/apply-undo-changes redo changes)
|
||||
undo-lib (tht/get-tokens-lib undo)]
|
||||
|
||||
;; Undo
|
||||
(t/is (some? (ctob/get-hidden-theme undo-lib))))))
|
||||
(t/is (some? (ctob/get-token-in-set undo-lib set-name token-name)))
|
||||
(t/is (nil? (ctob/get-token-in-set undo-lib new-set-name token-name)))
|
||||
;; Redo
|
||||
(t/is (nil? (ctob/get-token-in-set redo-lib set-name token-name)))
|
||||
(t/is (some? (ctob/get-token-in-set redo-lib new-set-name token-name))))))
|
||||
|
||||
(t/deftest generate-toggle-token-set-group-test
|
||||
(t/testing "toggling set group with no active sets inside will activate all child sets"
|
||||
@@ -81,7 +285,9 @@
|
||||
(ctob/add-set (ctob/make-token-set :name "foo/bar/baz/baz-child"))
|
||||
(ctob/add-theme (ctob/make-token-theme :name "theme"))
|
||||
(ctob/set-active-themes #{"/theme"})))
|
||||
changes (clt/generate-toggle-token-set-group (pcb/empty-changes) (tht/get-tokens-lib file) ["foo" "bar"])
|
||||
changes (-> (pcb/empty-changes)
|
||||
(pcb/with-library-data (:data file))
|
||||
(clt/generate-toggle-token-set-group (tht/get-tokens-lib file) ["foo" "bar"]))
|
||||
|
||||
redo (thf/apply-changes file changes)
|
||||
redo-lib (tht/get-tokens-lib redo)
|
||||
@@ -91,7 +297,6 @@
|
||||
(t/is (= #{"foo/bar/baz" "foo/bar/baz/baz-child"} (:sets (ctob/get-hidden-theme redo-lib))))
|
||||
|
||||
;; Undo
|
||||
(t/is (nil? (ctob/get-hidden-theme undo-lib)))
|
||||
(t/is (= #{"/theme"} (ctob/get-active-theme-paths undo-lib)))))
|
||||
|
||||
(t/testing "toggling set group with partially active sets inside will deactivate all child sets"
|
||||
@@ -103,7 +308,9 @@
|
||||
:sets #{"foo/bar/baz"}))
|
||||
(ctob/set-active-themes #{"/theme"})))
|
||||
|
||||
changes (clt/generate-toggle-token-set-group (pcb/empty-changes) (tht/get-tokens-lib file) ["foo" "bar"])
|
||||
changes (-> (pcb/empty-changes)
|
||||
(pcb/with-library-data (:data file))
|
||||
(clt/generate-toggle-token-set-group (tht/get-tokens-lib file) ["foo" "bar"]))
|
||||
|
||||
redo (thf/apply-changes file changes)
|
||||
redo-lib (tht/get-tokens-lib redo)
|
||||
@@ -113,7 +320,6 @@
|
||||
(t/is (= #{ctob/hidden-token-theme-path} (ctob/get-active-theme-paths redo-lib)))
|
||||
|
||||
;; Undo
|
||||
(t/is (nil? (ctob/get-hidden-theme undo-lib)))
|
||||
(t/is (= #{"/theme"} (ctob/get-active-theme-paths undo-lib))))))
|
||||
|
||||
(t/deftest generate-move-token-set-test
|
||||
|
||||
194
common/test/common_tests/logic/variants_test.cljc
Normal file
194
common/test/common_tests/logic/variants_test.cljc
Normal file
@@ -0,0 +1,194 @@
|
||||
;; 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.logic.variants-test
|
||||
(:require
|
||||
[app.common.files.changes-builder :as pcb]
|
||||
[app.common.logic.variants :as clv]
|
||||
[app.common.test-helpers.components :as thc]
|
||||
[app.common.test-helpers.files :as thf]
|
||||
[app.common.test-helpers.ids-map :as thi]
|
||||
[app.common.test-helpers.shapes :as ths]
|
||||
[app.common.test-helpers.variants :as thv]
|
||||
[clojure.test :as t]))
|
||||
|
||||
(t/use-fixtures :each thi/test-fixture)
|
||||
|
||||
(t/deftest test-update-property-name
|
||||
(let [;; ==== Setup
|
||||
file (-> (thf/sample-file :file1)
|
||||
(thv/add-variant :v01 :c01 :m01 :c02 :m02))
|
||||
v-id (-> (ths/get-shape file :v01) :id)
|
||||
page (thf/current-page file)
|
||||
|
||||
;; ==== Action
|
||||
changes (-> (pcb/empty-changes nil)
|
||||
(pcb/with-page-id (:id page))
|
||||
(pcb/with-library-data (:data file))
|
||||
(pcb/with-objects (:objects page))
|
||||
(clv/generate-update-property-name v-id 0 "NewName1")
|
||||
(clv/generate-update-property-name v-id 1 "NewName2"))
|
||||
|
||||
|
||||
file' (thf/apply-changes file changes)
|
||||
|
||||
|
||||
|
||||
;; ==== Get
|
||||
comp01' (thc/get-component file' :c01)
|
||||
comp02' (thc/get-component file' :c02)]
|
||||
|
||||
;; ==== Check
|
||||
(t/is (= (-> comp01' :variant-properties first :name) "NewName1"))
|
||||
(t/is (= (-> comp01' :variant-properties last :name) "NewName2"))
|
||||
(t/is (= (-> comp02' :variant-properties first :name) "NewName1"))
|
||||
(t/is (= (-> comp02' :variant-properties last :name) "NewName2"))))
|
||||
|
||||
|
||||
|
||||
(t/deftest test-add-new-property-without-values
|
||||
(let [;; ==== Setup
|
||||
file (-> (thf/sample-file :file1)
|
||||
(thv/add-variant :v01 :c01 :m01 :c02 :m02))
|
||||
v-id (-> (ths/get-shape file :v01) :id)
|
||||
page (thf/current-page file)
|
||||
|
||||
comp01 (thc/get-component file :c01)
|
||||
comp02 (thc/get-component file :c02)
|
||||
|
||||
|
||||
;; ==== Action
|
||||
changes (-> (pcb/empty-changes nil)
|
||||
(pcb/with-page-id (:id page))
|
||||
(pcb/with-library-data (:data file))
|
||||
(pcb/with-objects (:objects page))
|
||||
(clv/generate-add-new-property v-id))
|
||||
|
||||
|
||||
file' (thf/apply-changes file changes)
|
||||
|
||||
|
||||
|
||||
;; ==== Get
|
||||
comp01' (thc/get-component file' :c01)
|
||||
comp02' (thc/get-component file' :c02)]
|
||||
|
||||
;; ==== Check
|
||||
(t/is (= (count (:variant-properties comp01)) 1))
|
||||
(t/is (= (count (:variant-properties comp01')) 2))
|
||||
(t/is (= (count (:variant-properties comp02)) 1))
|
||||
(t/is (= (count (:variant-properties comp02')) 2))
|
||||
(t/is (= (-> comp01' :variant-properties last :value) ""))))
|
||||
|
||||
|
||||
|
||||
(t/deftest test-add-new-property-with-values
|
||||
(let [;; ==== Setup
|
||||
file (-> (thf/sample-file :file1)
|
||||
(thv/add-variant :v01 :c01 :m01 :c02 :m02))
|
||||
v-id (-> (ths/get-shape file :v01) :id)
|
||||
page (thf/current-page file)
|
||||
|
||||
comp01 (thc/get-component file :c01)
|
||||
comp02 (thc/get-component file :c02)
|
||||
|
||||
|
||||
;; ==== Action
|
||||
changes (-> (pcb/empty-changes nil)
|
||||
(pcb/with-page-id (:id page))
|
||||
(pcb/with-library-data (:data file))
|
||||
(pcb/with-objects (:objects page))
|
||||
(clv/generate-add-new-property v-id {:fill-values? true}))
|
||||
|
||||
|
||||
file' (thf/apply-changes file changes)
|
||||
|
||||
|
||||
|
||||
;; ==== Get
|
||||
comp01' (thc/get-component file' :c01)
|
||||
comp02' (thc/get-component file' :c02)]
|
||||
|
||||
;; ==== Check
|
||||
(t/is (= (count (:variant-properties comp01)) 1))
|
||||
(t/is (= (count (:variant-properties comp01')) 2))
|
||||
(t/is (= (count (:variant-properties comp02)) 1))
|
||||
(t/is (= (count (:variant-properties comp02')) 2))
|
||||
(t/is (= (-> comp01' :variant-properties last :value) "Value1"))))
|
||||
|
||||
|
||||
|
||||
(t/deftest test-remove-property
|
||||
(let [;; ==== Setup
|
||||
file (-> (thf/sample-file :file1)
|
||||
(thv/add-variant :v01 :c01 :m01 :c02 :m02))
|
||||
v-id (-> (ths/get-shape file :v01) :id)
|
||||
page (thf/current-page file)
|
||||
|
||||
changes (-> (pcb/empty-changes nil)
|
||||
(pcb/with-page-id (:id page))
|
||||
(pcb/with-library-data (:data file))
|
||||
(pcb/with-objects (:objects page))
|
||||
(clv/generate-add-new-property v-id))
|
||||
|
||||
|
||||
file (thf/apply-changes file changes)
|
||||
page (thf/current-page file)
|
||||
|
||||
comp01 (thc/get-component file :c01)
|
||||
comp02 (thc/get-component file :c02)
|
||||
|
||||
|
||||
;; ==== Action
|
||||
changes (-> (pcb/empty-changes nil)
|
||||
(pcb/with-page-id (:id page))
|
||||
(pcb/with-library-data (:data file))
|
||||
(pcb/with-objects (:objects page))
|
||||
(clv/generate-remove-property v-id 0))
|
||||
|
||||
|
||||
file' (thf/apply-changes file changes)
|
||||
|
||||
|
||||
|
||||
;; ==== Get
|
||||
comp01' (thc/get-component file' :c01)
|
||||
comp02' (thc/get-component file' :c02)]
|
||||
|
||||
;; ==== Check
|
||||
(t/is (= (count (:variant-properties comp01)) 2))
|
||||
(t/is (= (count (:variant-properties comp01')) 1))
|
||||
(t/is (= (count (:variant-properties comp02)) 2))
|
||||
(t/is (= (count (:variant-properties comp02')) 1))
|
||||
(t/is (= (-> comp01' :variant-properties first :name) "Property2"))))
|
||||
|
||||
(t/deftest test-update-property-value
|
||||
(let [;; ==== Setup
|
||||
file (-> (thf/sample-file :file1)
|
||||
(thv/add-variant :v01 :c01 :m01 :c02 :m02))
|
||||
|
||||
page (thf/current-page file)
|
||||
|
||||
comp01 (thc/get-component file :c01)
|
||||
comp02 (thc/get-component file :c02)
|
||||
|
||||
;; ==== Action
|
||||
changes (-> (pcb/empty-changes nil)
|
||||
(pcb/with-page-id (:id page))
|
||||
(pcb/with-library-data (:data file))
|
||||
(pcb/with-objects (:objects page))
|
||||
(clv/generate-update-property-value (:id comp01) 0 "NewValue1")
|
||||
(clv/generate-update-property-value (:id comp02) 0 "NewValue2"))
|
||||
|
||||
file' (thf/apply-changes file changes)
|
||||
|
||||
;; ==== Get
|
||||
comp01' (thc/get-component file' :c01)
|
||||
comp02' (thc/get-component file' :c02)]
|
||||
|
||||
;; ==== Check
|
||||
(t/is (= (-> comp01' :variant-properties first :value) "NewValue1"))
|
||||
(t/is (= (-> comp02' :variant-properties first :value) "NewValue2"))))
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"dark":
|
||||
{"small":
|
||||
{"$value": "8",
|
||||
"$type":"borderRadius",
|
||||
"$description":""}},
|
||||
"$themes": [],
|
||||
"$metadata":
|
||||
{"tokenSetOrder": ["dark"],
|
||||
"activeThemes": [],
|
||||
"activeSets": ["dark"]}}
|
||||
@@ -805,6 +805,8 @@
|
||||
"selectedTokenSets": {"light": "enabled"}
|
||||
} ],
|
||||
"$metadata": {
|
||||
"tokenSetOrder": ["core", "light", "dark", "theme"]
|
||||
"tokenSetOrder": ["core", "light", "dark", "theme"],
|
||||
"activeSets": {},
|
||||
"activeThemes": {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,810 @@
|
||||
{
|
||||
"core": {
|
||||
"dimension": {
|
||||
"scale": {
|
||||
"value": "2",
|
||||
"type": "dimension"
|
||||
},
|
||||
"xs": {
|
||||
"value": "4",
|
||||
"type": "dimension"
|
||||
},
|
||||
"sm": {
|
||||
"value": "{dimension.xs} * {dimension.scale}",
|
||||
"type": "dimension"
|
||||
},
|
||||
"md": {
|
||||
"value": "{dimension.sm} * {dimension.scale}",
|
||||
"type": "dimension"
|
||||
},
|
||||
"lg": {
|
||||
"value": "{dimension.md} * {dimension.scale}",
|
||||
"type": "dimension"
|
||||
},
|
||||
"xl": {
|
||||
"value": "{dimension.lg} * {dimension.scale}",
|
||||
"type": "dimension"
|
||||
}
|
||||
},
|
||||
"spacing": {
|
||||
"xs": {
|
||||
"value": "{dimension.xs}",
|
||||
"type": "spacing"
|
||||
},
|
||||
"sm": {
|
||||
"value": "{dimension.sm}",
|
||||
"type": "spacing"
|
||||
},
|
||||
"md": {
|
||||
"value": "{dimension.md}",
|
||||
"type": "spacing"
|
||||
},
|
||||
"lg": {
|
||||
"value": "{dimension.lg}",
|
||||
"type": "spacing"
|
||||
},
|
||||
"xl": {
|
||||
"value": "{dimension.xl}",
|
||||
"type": "spacing"
|
||||
},
|
||||
"multi-value": {
|
||||
"value": "{dimension.sm} {dimension.xl}",
|
||||
"type": "spacing",
|
||||
"$description": "You can have multiple values in a single spacing token"
|
||||
}
|
||||
},
|
||||
"borderRadius": {
|
||||
"sm": {
|
||||
"value": "4",
|
||||
"type": "borderRadius"
|
||||
},
|
||||
"lg": {
|
||||
"value": "8",
|
||||
"type": "borderRadius"
|
||||
},
|
||||
"xl": {
|
||||
"value": "16",
|
||||
"type": "borderRadius"
|
||||
},
|
||||
"multi-value": {
|
||||
"value": "{borderRadius.sm} {borderRadius.lg}",
|
||||
"type": "borderRadius",
|
||||
"$description": "You can have multiple values in a single radius token. Read more on these: https://docs.tokens.studio/available-tokens/border-radius-tokens#single--multiple-values"
|
||||
}
|
||||
},
|
||||
"colors": {
|
||||
"black": {
|
||||
"value": "#000000",
|
||||
"type": "color"
|
||||
},
|
||||
"white": {
|
||||
"value": "#ffffff",
|
||||
"type": "color"
|
||||
},
|
||||
"gray": {
|
||||
"100": {
|
||||
"value": "#f7fafc",
|
||||
"type": "color"
|
||||
},
|
||||
"200": {
|
||||
"value": "#edf2f7",
|
||||
"type": "color"
|
||||
},
|
||||
"300": {
|
||||
"value": "#e2e8f0",
|
||||
"type": "color"
|
||||
},
|
||||
"400": {
|
||||
"value": "#cbd5e0",
|
||||
"type": "color"
|
||||
},
|
||||
"500": {
|
||||
"value": "#a0aec0",
|
||||
"type": "color"
|
||||
},
|
||||
"600": {
|
||||
"value": "#718096",
|
||||
"type": "color"
|
||||
},
|
||||
"700": {
|
||||
"value": "#4a5568",
|
||||
"type": "color"
|
||||
},
|
||||
"800": {
|
||||
"value": "#2d3748",
|
||||
"type": "color"
|
||||
},
|
||||
"900": {
|
||||
"value": "#1a202c",
|
||||
"type": "color"
|
||||
}
|
||||
},
|
||||
"red": {
|
||||
"100": {
|
||||
"value": "#fff5f5",
|
||||
"type": "color"
|
||||
},
|
||||
"200": {
|
||||
"value": "#fed7d7",
|
||||
"type": "color"
|
||||
},
|
||||
"300": {
|
||||
"value": "#feb2b2",
|
||||
"type": "color"
|
||||
},
|
||||
"400": {
|
||||
"value": "#fc8181",
|
||||
"type": "color"
|
||||
},
|
||||
"500": {
|
||||
"value": "#f56565",
|
||||
"type": "color"
|
||||
},
|
||||
"600": {
|
||||
"value": "#e53e3e",
|
||||
"type": "color"
|
||||
},
|
||||
"700": {
|
||||
"value": "#c53030",
|
||||
"type": "color"
|
||||
},
|
||||
"800": {
|
||||
"value": "#9b2c2c",
|
||||
"type": "color"
|
||||
},
|
||||
"900": {
|
||||
"value": "#742a2a",
|
||||
"type": "color"
|
||||
}
|
||||
},
|
||||
"orange": {
|
||||
"100": {
|
||||
"value": "#fffaf0",
|
||||
"type": "color"
|
||||
},
|
||||
"200": {
|
||||
"value": "#feebc8",
|
||||
"type": "color"
|
||||
},
|
||||
"300": {
|
||||
"value": "#fbd38d",
|
||||
"type": "color"
|
||||
},
|
||||
"400": {
|
||||
"value": "#f6ad55",
|
||||
"type": "color"
|
||||
},
|
||||
"500": {
|
||||
"value": "#ed8936",
|
||||
"type": "color"
|
||||
},
|
||||
"600": {
|
||||
"value": "#dd6b20",
|
||||
"type": "color"
|
||||
},
|
||||
"700": {
|
||||
"value": "#c05621",
|
||||
"type": "color"
|
||||
},
|
||||
"800": {
|
||||
"value": "#9c4221",
|
||||
"type": "color"
|
||||
},
|
||||
"900": {
|
||||
"value": "#7b341e",
|
||||
"type": "color"
|
||||
}
|
||||
},
|
||||
"yellow": {
|
||||
"100": {
|
||||
"value": "#fffff0",
|
||||
"type": "color"
|
||||
},
|
||||
"200": {
|
||||
"value": "#fefcbf",
|
||||
"type": "color"
|
||||
},
|
||||
"300": {
|
||||
"value": "#faf089",
|
||||
"type": "color"
|
||||
},
|
||||
"400": {
|
||||
"value": "#f6e05e",
|
||||
"type": "color"
|
||||
},
|
||||
"500": {
|
||||
"value": "#ecc94b",
|
||||
"type": "color"
|
||||
},
|
||||
"600": {
|
||||
"value": "#d69e2e",
|
||||
"type": "color"
|
||||
},
|
||||
"700": {
|
||||
"value": "#b7791f",
|
||||
"type": "color"
|
||||
},
|
||||
"800": {
|
||||
"value": "#975a16",
|
||||
"type": "color"
|
||||
},
|
||||
"900": {
|
||||
"value": "#744210",
|
||||
"type": "color"
|
||||
}
|
||||
},
|
||||
"green": {
|
||||
"100": {
|
||||
"value": "#f0fff4",
|
||||
"type": "color"
|
||||
},
|
||||
"200": {
|
||||
"value": "#c6f6d5",
|
||||
"type": "color"
|
||||
},
|
||||
"300": {
|
||||
"value": "#9ae6b4",
|
||||
"type": "color"
|
||||
},
|
||||
"400": {
|
||||
"value": "#68d391",
|
||||
"type": "color"
|
||||
},
|
||||
"500": {
|
||||
"value": "#48bb78",
|
||||
"type": "color"
|
||||
},
|
||||
"600": {
|
||||
"value": "#38a169",
|
||||
"type": "color"
|
||||
},
|
||||
"700": {
|
||||
"value": "#2f855a",
|
||||
"type": "color"
|
||||
},
|
||||
"800": {
|
||||
"value": "#276749",
|
||||
"type": "color"
|
||||
},
|
||||
"900": {
|
||||
"value": "#22543d",
|
||||
"type": "color"
|
||||
}
|
||||
},
|
||||
"teal": {
|
||||
"100": {
|
||||
"value": "#e6fffa",
|
||||
"type": "color"
|
||||
},
|
||||
"200": {
|
||||
"value": "#b2f5ea",
|
||||
"type": "color"
|
||||
},
|
||||
"300": {
|
||||
"value": "#81e6d9",
|
||||
"type": "color"
|
||||
},
|
||||
"400": {
|
||||
"value": "#4fd1c5",
|
||||
"type": "color"
|
||||
},
|
||||
"500": {
|
||||
"value": "#38b2ac",
|
||||
"type": "color"
|
||||
},
|
||||
"600": {
|
||||
"value": "#319795",
|
||||
"type": "color"
|
||||
},
|
||||
"700": {
|
||||
"value": "#2c7a7b",
|
||||
"type": "color"
|
||||
},
|
||||
"800": {
|
||||
"value": "#285e61",
|
||||
"type": "color"
|
||||
},
|
||||
"900": {
|
||||
"value": "#234e52",
|
||||
"type": "color"
|
||||
}
|
||||
},
|
||||
"blue": {
|
||||
"100": {
|
||||
"value": "#ebf8ff",
|
||||
"type": "color"
|
||||
},
|
||||
"200": {
|
||||
"value": "#bee3f8",
|
||||
"type": "color"
|
||||
},
|
||||
"300": {
|
||||
"value": "#90cdf4",
|
||||
"type": "color"
|
||||
},
|
||||
"400": {
|
||||
"value": "#63b3ed",
|
||||
"type": "color"
|
||||
},
|
||||
"500": {
|
||||
"value": "#4299e1",
|
||||
"type": "color"
|
||||
},
|
||||
"600": {
|
||||
"value": "#3182ce",
|
||||
"type": "color"
|
||||
},
|
||||
"700": {
|
||||
"value": "#2b6cb0",
|
||||
"type": "color"
|
||||
},
|
||||
"800": {
|
||||
"value": "#2c5282",
|
||||
"type": "color"
|
||||
},
|
||||
"900": {
|
||||
"value": "#2a4365",
|
||||
"type": "color"
|
||||
}
|
||||
},
|
||||
"indigo": {
|
||||
"100": {
|
||||
"value": "#ebf4ff",
|
||||
"type": "color"
|
||||
},
|
||||
"200": {
|
||||
"value": "#c3dafe",
|
||||
"type": "color"
|
||||
},
|
||||
"300": {
|
||||
"value": "#a3bffa",
|
||||
"type": "color"
|
||||
},
|
||||
"400": {
|
||||
"value": "#7f9cf5",
|
||||
"type": "color"
|
||||
},
|
||||
"500": {
|
||||
"value": "#667eea",
|
||||
"type": "color"
|
||||
},
|
||||
"600": {
|
||||
"value": "#5a67d8",
|
||||
"type": "color"
|
||||
},
|
||||
"700": {
|
||||
"value": "#4c51bf",
|
||||
"type": "color"
|
||||
},
|
||||
"800": {
|
||||
"value": "#434190",
|
||||
"type": "color"
|
||||
},
|
||||
"900": {
|
||||
"value": "#3c366b",
|
||||
"type": "color"
|
||||
}
|
||||
},
|
||||
"purple": {
|
||||
"100": {
|
||||
"value": "#faf5ff",
|
||||
"type": "color"
|
||||
},
|
||||
"200": {
|
||||
"value": "#e9d8fd",
|
||||
"type": "color"
|
||||
},
|
||||
"300": {
|
||||
"value": "#d6bcfa",
|
||||
"type": "color"
|
||||
},
|
||||
"400": {
|
||||
"value": "#b794f4",
|
||||
"type": "color"
|
||||
},
|
||||
"500": {
|
||||
"value": "#9f7aea",
|
||||
"type": "color"
|
||||
},
|
||||
"600": {
|
||||
"value": "#805ad5",
|
||||
"type": "color"
|
||||
},
|
||||
"700": {
|
||||
"value": "#6b46c1",
|
||||
"type": "color"
|
||||
},
|
||||
"800": {
|
||||
"value": "#553c9a",
|
||||
"type": "color"
|
||||
},
|
||||
"900": {
|
||||
"value": "#44337a",
|
||||
"type": "color"
|
||||
}
|
||||
},
|
||||
"pink": {
|
||||
"100": {
|
||||
"value": "#fff5f7",
|
||||
"type": "color"
|
||||
},
|
||||
"200": {
|
||||
"value": "#fed7e2",
|
||||
"type": "color"
|
||||
},
|
||||
"300": {
|
||||
"value": "#fbb6ce",
|
||||
"type": "color"
|
||||
},
|
||||
"400": {
|
||||
"value": "#f687b3",
|
||||
"type": "color"
|
||||
},
|
||||
"500": {
|
||||
"value": "#ed64a6",
|
||||
"type": "color"
|
||||
},
|
||||
"600": {
|
||||
"value": "#d53f8c",
|
||||
"type": "color"
|
||||
},
|
||||
"700": {
|
||||
"value": "#b83280",
|
||||
"type": "color"
|
||||
},
|
||||
"800": {
|
||||
"value": "#97266d",
|
||||
"type": "color"
|
||||
},
|
||||
"900": {
|
||||
"value": "#702459",
|
||||
"type": "color"
|
||||
}
|
||||
}
|
||||
},
|
||||
"opacity": {
|
||||
"low": {
|
||||
"value": "10%",
|
||||
"type": "opacity"
|
||||
},
|
||||
"md": {
|
||||
"value": "50%",
|
||||
"type": "opacity"
|
||||
},
|
||||
"high": {
|
||||
"value": "90%",
|
||||
"type": "opacity"
|
||||
}
|
||||
},
|
||||
"fontFamilies": {
|
||||
"heading": {
|
||||
"value": "Inter",
|
||||
"type": "fontFamilies"
|
||||
},
|
||||
"body": {
|
||||
"value": "Roboto",
|
||||
"type": "fontFamilies"
|
||||
}
|
||||
},
|
||||
"lineHeights": {
|
||||
"heading": {
|
||||
"value": "110%",
|
||||
"type": "lineHeights"
|
||||
},
|
||||
"body": {
|
||||
"value": "140%",
|
||||
"type": "lineHeights"
|
||||
}
|
||||
},
|
||||
"letterSpacing": {
|
||||
"default": {
|
||||
"value": "0",
|
||||
"type": "letterSpacing"
|
||||
},
|
||||
"increased": {
|
||||
"value": "150%",
|
||||
"type": "letterSpacing"
|
||||
},
|
||||
"decreased": {
|
||||
"value": "-5%",
|
||||
"type": "letterSpacing"
|
||||
}
|
||||
},
|
||||
"paragraphSpacing": {
|
||||
"h1": {
|
||||
"value": "32",
|
||||
"type": "paragraphSpacing"
|
||||
},
|
||||
"h2": {
|
||||
"value": "26",
|
||||
"type": "paragraphSpacing"
|
||||
}
|
||||
},
|
||||
"fontWeights": {
|
||||
"headingRegular": {
|
||||
"value": "Regular",
|
||||
"type": "fontWeights"
|
||||
},
|
||||
"headingBold": {
|
||||
"value": "Bold",
|
||||
"type": "fontWeights"
|
||||
},
|
||||
"bodyRegular": {
|
||||
"value": "Regular",
|
||||
"type": "fontWeights"
|
||||
},
|
||||
"bodyBold": {
|
||||
"value": "Bold",
|
||||
"type": "fontWeights"
|
||||
}
|
||||
},
|
||||
"fontSizes": {
|
||||
"h1": {
|
||||
"value": "{fontSizes.h2} * 1.25",
|
||||
"type": "fontSizes"
|
||||
},
|
||||
"h2": {
|
||||
"value": "{fontSizes.h3} * 1.25",
|
||||
"type": "fontSizes"
|
||||
},
|
||||
"h3": {
|
||||
"value": "{fontSizes.h4} * 1.25",
|
||||
"type": "fontSizes"
|
||||
},
|
||||
"h4": {
|
||||
"value": "{fontSizes.h5} * 1.25",
|
||||
"type": "fontSizes"
|
||||
},
|
||||
"h5": {
|
||||
"value": "{fontSizes.h6} * 1.25",
|
||||
"type": "fontSizes"
|
||||
},
|
||||
"h6": {
|
||||
"value": "{fontSizes.body} * 1",
|
||||
"type": "fontSizes"
|
||||
},
|
||||
"body": {
|
||||
"value": "16",
|
||||
"type": "fontSizes"
|
||||
},
|
||||
"sm": {
|
||||
"value": "{fontSizes.body} * 0.85",
|
||||
"type": "fontSizes"
|
||||
},
|
||||
"xs": {
|
||||
"value": "{fontSizes.body} * 0.65",
|
||||
"type": "fontSizes"
|
||||
}
|
||||
}
|
||||
},
|
||||
"light": {
|
||||
"fg": {
|
||||
"default": {
|
||||
"value": "{colors.black}",
|
||||
"type": "color"
|
||||
},
|
||||
"muted": {
|
||||
"value": "{colors.gray.700}",
|
||||
"type": "color"
|
||||
},
|
||||
"subtle": {
|
||||
"value": "{colors.gray.500}",
|
||||
"type": "color"
|
||||
}
|
||||
},
|
||||
"bg": {
|
||||
"default": {
|
||||
"value": "{colors.white}",
|
||||
"type": "color"
|
||||
},
|
||||
"muted": {
|
||||
"value": "{colors.gray.100}",
|
||||
"type": "color"
|
||||
},
|
||||
"subtle": {
|
||||
"value": "{colors.gray.200}",
|
||||
"type": "color"
|
||||
}
|
||||
},
|
||||
"accent": {
|
||||
"default": {
|
||||
"value": "{colors.indigo.400}",
|
||||
"type": "color"
|
||||
},
|
||||
"onAccent": {
|
||||
"value": "{colors.white}",
|
||||
"type": "color"
|
||||
},
|
||||
"bg": {
|
||||
"value": "{colors.indigo.200}",
|
||||
"type": "color"
|
||||
}
|
||||
},
|
||||
"shadows": {
|
||||
"default": {
|
||||
"value": "{colors.gray.900}",
|
||||
"type": "color"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dark": {
|
||||
"fg": {
|
||||
"default": {
|
||||
"value": "{colors.white}",
|
||||
"type": "color"
|
||||
},
|
||||
"muted": {
|
||||
"value": "{colors.gray.300}",
|
||||
"type": "color"
|
||||
},
|
||||
"subtle": {
|
||||
"value": "{colors.gray.500}",
|
||||
"type": "color"
|
||||
}
|
||||
},
|
||||
"bg": {
|
||||
"default": {
|
||||
"value": "{colors.gray.900}",
|
||||
"type": "color"
|
||||
},
|
||||
"muted": {
|
||||
"value": "{colors.gray.700}",
|
||||
"type": "color"
|
||||
},
|
||||
"subtle": {
|
||||
"value": "{colors.gray.600}",
|
||||
"type": "color"
|
||||
}
|
||||
},
|
||||
"accent": {
|
||||
"default": {
|
||||
"value": "{colors.indigo.600}",
|
||||
"type": "color"
|
||||
},
|
||||
"onAccent": {
|
||||
"value": "{colors.white}",
|
||||
"type": "color"
|
||||
},
|
||||
"bg": {
|
||||
"value": "{colors.indigo.800}",
|
||||
"type": "color"
|
||||
}
|
||||
},
|
||||
"shadows": {
|
||||
"default": {
|
||||
"value": "rgba({colors.black}, 0.3)",
|
||||
"type": "color"
|
||||
}
|
||||
}
|
||||
},
|
||||
"theme": {
|
||||
"button": {
|
||||
"primary": {
|
||||
"background": {
|
||||
"value": "{accent.default}",
|
||||
"type": "color"
|
||||
},
|
||||
"text": {
|
||||
"value": "{accent.onAccent}",
|
||||
"type": "color"
|
||||
}
|
||||
},
|
||||
"borderRadius": {
|
||||
"value": "{borderRadius.lg}",
|
||||
"type": "borderRadius"
|
||||
},
|
||||
"borderWidth": {
|
||||
"value": "{dimension.sm}",
|
||||
"type": "borderWidth"
|
||||
}
|
||||
},
|
||||
"card": {
|
||||
"borderRadius": {
|
||||
"value": "{borderRadius.lg}",
|
||||
"type": "borderRadius"
|
||||
},
|
||||
"background": {
|
||||
"value": "{bg.default}",
|
||||
"type": "color"
|
||||
},
|
||||
"padding": {
|
||||
"value": "{dimension.md}",
|
||||
"type": "dimension"
|
||||
}
|
||||
},
|
||||
"boxShadow": {
|
||||
"default": {
|
||||
"value": [
|
||||
{
|
||||
"x": 5,
|
||||
"y": 5,
|
||||
"spread": 3,
|
||||
"color": "rgba({shadows.default}, 0.15)",
|
||||
"blur": 5,
|
||||
"type": "dropShadow"
|
||||
},
|
||||
{
|
||||
"x": 4,
|
||||
"y": 4,
|
||||
"spread": 6,
|
||||
"color": "#00000033",
|
||||
"blur": 5,
|
||||
"type": "innerShadow"
|
||||
}
|
||||
],
|
||||
"type": "boxShadow"
|
||||
}
|
||||
},
|
||||
"typography": {
|
||||
"H1": {
|
||||
"Bold": {
|
||||
"value": {
|
||||
"fontFamily": "{fontFamilies.heading}",
|
||||
"fontWeight": "{fontWeights.headingBold}",
|
||||
"lineHeight": "{lineHeights.heading}",
|
||||
"fontSize": "{fontSizes.h1}",
|
||||
"paragraphSpacing": "{paragraphSpacing.h1}",
|
||||
"letterSpacing": "{letterSpacing.decreased}"
|
||||
},
|
||||
"type": "typography"
|
||||
},
|
||||
"Regular": {
|
||||
"value": {
|
||||
"fontFamily": "{fontFamilies.heading}",
|
||||
"fontWeight": "{fontWeights.headingRegular}",
|
||||
"lineHeight": "{lineHeights.heading}",
|
||||
"fontSize": "{fontSizes.h1}",
|
||||
"paragraphSpacing": "{paragraphSpacing.h1}",
|
||||
"letterSpacing": "{letterSpacing.decreased}"
|
||||
},
|
||||
"type": "typography"
|
||||
}
|
||||
},
|
||||
"H2": {
|
||||
"Bold": {
|
||||
"value": {
|
||||
"fontFamily": "{fontFamilies.heading}",
|
||||
"fontWeight": "{fontWeights.headingBold}",
|
||||
"lineHeight": "{lineHeights.heading}",
|
||||
"fontSize": "{fontSizes.h2}",
|
||||
"paragraphSpacing": "{paragraphSpacing.h2}",
|
||||
"letterSpacing": "{letterSpacing.decreased}"
|
||||
},
|
||||
"type": "typography"
|
||||
},
|
||||
"Regular": {
|
||||
"value": {
|
||||
"fontFamily": "{fontFamilies.heading}",
|
||||
"fontWeight": "{fontWeights.headingRegular}",
|
||||
"lineHeight": "{lineHeights.heading}",
|
||||
"fontSize": "{fontSizes.h2}",
|
||||
"paragraphSpacing": "{paragraphSpacing.h2}",
|
||||
"letterSpacing": "{letterSpacing.decreased}"
|
||||
},
|
||||
"type": "typography"
|
||||
}
|
||||
},
|
||||
"Body": {
|
||||
"value": {
|
||||
"fontFamily": "{fontFamilies.body}",
|
||||
"fontWeight": "{fontWeights.bodyRegular}",
|
||||
"lineHeight": "{lineHeights.heading}",
|
||||
"fontSize": "{fontSizes.body}",
|
||||
"paragraphSpacing": "{paragraphSpacing.h2}"
|
||||
},
|
||||
"type": "typography"
|
||||
}
|
||||
}
|
||||
},
|
||||
"$themes": [ {
|
||||
"name": "theme-1",
|
||||
"group": "group-1",
|
||||
"description": null,
|
||||
"is-source": false,
|
||||
"modified-at": "2024-01-01T00:00:00.000+00:00",
|
||||
"selectedTokenSets": {"light": "enabled"}
|
||||
} ],
|
||||
"$metadata": {
|
||||
"tokenSetOrder": ["core", "light", "dark", "theme"]
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,7 @@ ENV NODE_VERSION=v22.13.1 \
|
||||
BABASHKA_VERSION=1.12.196 \
|
||||
CLJFMT_VERSION=0.13.0 \
|
||||
RUSTUP_VERSION=1.27.1 \
|
||||
RUST_VERSION=1.82.0 \
|
||||
RUST_VERSION=1.85.0 \
|
||||
LANG=en_US.UTF-8 \
|
||||
LC_ALL=en_US.UTF-8
|
||||
|
||||
@@ -264,7 +264,8 @@ RUN set -eux; \
|
||||
./rustup-init -y --no-modify-path --profile minimal --default-toolchain $RUST_VERSION --default-host ${rustArch}; \
|
||||
rm rustup-init; \
|
||||
chmod -R a+w $RUSTUP_HOME $CARGO_HOME; \
|
||||
rustup component add rustfmt;
|
||||
rustup component add rustfmt; \
|
||||
rustup component add clippy;
|
||||
|
||||
WORKDIR /usr/local
|
||||
|
||||
|
||||
@@ -489,6 +489,26 @@ PENPOT_STORAGE_ASSETS_S3_ENDPOINT: <endpoint-uri>
|
||||
These settings are equally useful if you have a Minio storage system.
|
||||
</p>
|
||||
|
||||
### Autosave
|
||||
|
||||
By default, Penpot stores manually saved versions indefinitely; these can be found in the History tab and can be renamed, restored, deleted, etc. Additionally, the default behavior of on-premise instances is to not keep automatic version history. This automatic behavior can be modified and adapted to each on-premise installation with the corresponding configuration.
|
||||
|
||||
<p class="advice">
|
||||
You need to be very careful when configuring automatic versioning, as it can significantly impact the size of your database. If you configure automatic versioning, you'll need to monitor this impact; if you're unsure about this management, we recommend leaving the default settings and using manual versioning.
|
||||
</p>
|
||||
|
||||
This is how configuration looks for auto-file-snapshot
|
||||
|
||||
```bash
|
||||
PENPOT_FLAGS: enable-auto-file-snapshot # Enable automatic version saving
|
||||
|
||||
# Backend
|
||||
PENPOT_AUTO_FILE_SNAPSHOT_EVERY: 5 # How many save operations trigger the auto-save-version?
|
||||
PENPOT_AUTO_FILE_SNAPSHOT_TIIMEOUT: "1h" # How often is an automatic save forced even if the `every` trigger is not met?
|
||||
```
|
||||
|
||||
Setting custom values for auto-file-snapshot does not change the behaviour for manual versions.
|
||||
|
||||
## Frontend
|
||||
|
||||
In comparison with backend, frontend only has a small number of runtime configuration
|
||||
|
||||
@@ -157,3 +157,11 @@ similar to a webmail client. Simply navigate to:
|
||||
|
||||
[http://localhost:1080](http://localhost:1080)
|
||||
|
||||
## Team Feature Flags
|
||||
|
||||
To test a Feature Flag, you can enable or disable them by team through the `dbg` page:
|
||||
|
||||
1. Create a new team or navigate to an existing team in Penpot.
|
||||
2. Copy the `team-id` from the URL (e.g., `?team-id=1234bd95-69dd-805c-8005-c015415436ae`). If no team is selected, the default profile team will be used.
|
||||
3. Go to [http://localhost:3449/dbg](http://localhost:3449/dbg).
|
||||
4. Open the Feature Flag panel, enter the `team-id` and the `feature` name in either the enable or disable section, and click `Submit`.
|
||||
|
||||
@@ -302,6 +302,17 @@ Ensure your development environment docker image is up to date.
|
||||
|
||||
**NOTE** You can learn more about how to set up, start and stop our development environment [here](/technical-guide/developer/devenv)
|
||||
|
||||
#### Release mode
|
||||
|
||||
This is not required, but it may be convenient to compile Penpot in release mode before running the tests. This way they will be much quicker and stable. For this, go to the frontend window in the tmux session (<code class="language-bash">Ctrl + b 1</code>), interrupt the watch process with <code class="language-bash">Ctrl + C</code> and type:
|
||||
|
||||
```bash
|
||||
yarn run build:app
|
||||
```
|
||||
|
||||
Obviously, in this mode if you make changes to the source code, you will need to repeat the build manually each time. It may be useful to use wath mode when debugging a single test, and use release mode to run all the suite.
|
||||
|
||||
|
||||
### Running the integration tests
|
||||
|
||||
#### Headless mode
|
||||
@@ -344,6 +355,8 @@ npx playwright test --ui
|
||||
|
||||
> ❗️ **IMPORTANT**: You might need to [install Playwright's browsers and dependencies](https://playwright.dev/docs/intro) in your host machine with: <code class="language-bash">npx playwright install --with-deps</code>. In case you are using a Linux distribution other than Ubuntu, [you might need to install the dependencies manually](https://github.com/microsoft/playwright/issues/11122).
|
||||
|
||||
> You will also need yarn in your host nodejs. For this, do <code class="language-bash">corepack enable</code> and then just <code class="language-bash">yarn</code>.
|
||||
|
||||
### How to write a test
|
||||
|
||||
When writing integration tests, we are simulating user actions and events triggered by them, in other to mirror real-world user interactions. The difference with fully end-to-end tests is that here we are faking the backend by intercepting the network requests, so tests can run faster and more tied to the front-end.
|
||||
|
||||
@@ -27,7 +27,7 @@ title: 07· Exporting objects
|
||||
<ul>
|
||||
<li><strong>Size</strong> - Options for the most common sizing scales.</li>
|
||||
<li><strong>Suffix</strong> - Especially useful if you are exporting at different scales.</li>
|
||||
<li><strong>File format</strong> - PNG, SVG, JPEG, PDF.</li>
|
||||
<li><strong>File format</strong> - PNG, JPEG, WEBP, SVG, PDF.</li>
|
||||
</ul>
|
||||
|
||||
<h2 id="export-multiple-elements">Exporting multiple elements</h2>
|
||||
|
||||
@@ -166,7 +166,7 @@ a design.</p>
|
||||
|
||||
<h2 id="curves">Curves (freehand)</h2>
|
||||
<p>The curve tool allows a path to be created directly in a freehand mode.
|
||||
Select the curve tool by clicking on the icon at the toolbar or pressing <kbd>Ctrl/⌘</kbd> + <kbd>c</kbd>.
|
||||
Select the curve tool by clicking on the icon at the toolbar or pressing <kbd>Shift/⇧</kbd> + <kbd>c</kbd>.
|
||||
<p>The path created will contain a lot of points, but it is edited the same way as any other curve.</p>
|
||||
|
||||
<h2 id="paths">Paths (bezier)</h2>
|
||||
@@ -206,7 +206,7 @@ You can choose to edit individual nodes or create new ones. Press <kbd>Esc</kbd>
|
||||
<h3>Insert images</h3>
|
||||
<p>There are several options for inserting an image into a Penpot file:</p>
|
||||
<ul>
|
||||
<li>Use the <strong>image tool</strong> at the toolbar or press <kbd>K</kbd> to inspect images in your file system.</li>
|
||||
<li>Use the <strong>image tool</strong> at the toolbar or press <kbd>K</kbd> to insert images in your file system.</li>
|
||||
<li><strong>Drag</strong> an image from your computer to the viewport.</li>
|
||||
<li>Copy an image & paste it or drag it right from a <strong>browser</strong>.</li>
|
||||
<li>Drag an image from a Penpot <strong>library</strong>.</li>
|
||||
|
||||
@@ -129,7 +129,7 @@ title: 02· The interface
|
||||
<li><strong>Custom fonts:</strong> If you have purchased or own personal fonts that are not included in the catalog provided by Penpot, you can upload them from your computer and use them across the files of a team.</li>
|
||||
<li><strong>Pinned projects:</strong> If you want to keep some projects handy (for instance because you’re currently working on them) you can pin them to make them quickly available at the sidebar.</li>
|
||||
<li><strong>User area:</strong> This must be you! Access your profile settings, Penpot tutorials, the Penpot Community and more. You can also find here a way to leave us feedback. We’d love to read your thoughts :)</li>
|
||||
<li><strong>Comments notifications:</strong> Here you will be able to see if you have unread comments inside the files of the team.</li>
|
||||
<li><strong>Comments notifications:</strong> Here you will be able to see if you have unread comments inside the files of the team. There's also a button to mark all notifications as read.</li>
|
||||
<li><strong>Create project:</strong> Create as many projects as you need to organize your designs.</li>
|
||||
<li><strong>File card:</strong> Basic information about a file at plain sight. A preview, update info or if it’s added as a Shared Library. From there you can perform several actions over the file (rename, duplicate, move, download, delete).</li>
|
||||
<li><strong>Libraries & Templates module:</strong> A curated selection of Libraries & Templates files ready to import.</li>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"license": "MPL-2.0",
|
||||
"author": "Kaleidos INC",
|
||||
"private": true,
|
||||
"packageManager": "yarn@4.6.0+sha512.5383cc12567a95f1d668fbe762dfe0075c595b4bfff433be478dbbe24e05251a8e8c3eb992a986667c1d53b6c3a9c85b8398c35a960587fbd9fa3a0915406728",
|
||||
"packageManager": "yarn@4.7.0+sha512.5a0afa1d4c1d844b3447ee3319633797bcd6385d9a44be07993ae52ff4facabccafb4af5dcd1c2f9a94ac113e5e9ff56f6130431905884414229e284e37bb7c9",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/penpot/penpot"
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
(s/def ::name ::us/string)
|
||||
(s/def ::suffix ::us/string)
|
||||
(s/def ::type #{:jpeg :png :pdf :svg})
|
||||
(s/def ::type #{:png :jpeg :webp :pdf :svg})
|
||||
(s/def ::page-id ::us/uuid)
|
||||
(s/def ::file-id ::us/uuid)
|
||||
(s/def ::share-id ::us/uuid)
|
||||
@@ -40,6 +40,7 @@
|
||||
(case type
|
||||
:png (rb/render params on-object)
|
||||
:jpeg (rb/render params on-object)
|
||||
:webp (rb/render params on-object)
|
||||
:pdf (rp/render params on-object)
|
||||
:svg (rs/render params on-object)))
|
||||
|
||||
|
||||
@@ -34,7 +34,11 @@
|
||||
(bw/wait-for node)
|
||||
(case type
|
||||
:png (bw/screenshot node {:omit-background? true :type type :path path})
|
||||
:jpeg (bw/screenshot node {:omit-background? false :type type :path path}))
|
||||
:jpeg (bw/screenshot node {:omit-background? false :type type :path path})
|
||||
:webp (p/let [png-path (sh/tempfile :prefix "penpot.tmp.render.bitmap." :suffix ".png")]
|
||||
;; playwright only supports jpg and png, we need to convert it afterwards
|
||||
(bw/screenshot node {:omit-background? true :type :png :path png-path})
|
||||
(sh/run-cmd! (str "convert " png-path " -quality 100 WEBP:" path))))
|
||||
(on-object (assoc object :path path))))
|
||||
|
||||
(render [uri page]
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
(case type
|
||||
:png ".png"
|
||||
:jpeg ".jpg"
|
||||
:webp ".webp"
|
||||
:svg ".svg"
|
||||
:pdf ".pdf"
|
||||
:zip ".zip"))
|
||||
@@ -26,6 +27,7 @@
|
||||
:pdf "application/pdf"
|
||||
:svg "image/svg+xml"
|
||||
:jpeg "image/jpeg"
|
||||
:png "image/png"))
|
||||
:png "image/png"
|
||||
:webp "image/webp"))
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"license": "MPL-2.0",
|
||||
"author": "Kaleidos INC",
|
||||
"private": true,
|
||||
"packageManager": "yarn@4.6.0+sha512.5383cc12567a95f1d668fbe762dfe0075c595b4bfff433be478dbbe24e05251a8e8c3eb992a986667c1d53b6c3a9c85b8398c35a960587fbd9fa3a0915406728",
|
||||
"packageManager": "yarn@4.7.0+sha512.5a0afa1d4c1d844b3447ee3319633797bcd6385d9a44be07993ae52ff4facabccafb4af5dcd1c2f9a94ac113e5e9ff56f6130431905884414229e284e37bb7c9",
|
||||
"browserslist": [
|
||||
"defaults"
|
||||
],
|
||||
|
||||
@@ -34,6 +34,8 @@ export default defineConfig({
|
||||
trace: "on-first-retry",
|
||||
|
||||
locale: "en-US",
|
||||
|
||||
permissions: ["clipboard-write", "clipboard-read"],
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
[
|
||||
{
|
||||
"~:page-name":"Page 1",
|
||||
"~:file-id":"~ud192fd06-a3e6-80d5-8004-7b7aaaea2a23",
|
||||
"~:participants":{
|
||||
"~#set":[
|
||||
"~u0515a066-e303-8169-8004-73eb4018f4e0"
|
||||
]
|
||||
},
|
||||
"~:content":"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque lacus tellus, pretium id dapibus in, suscipit eu magna. Duis rhoncus, nisl quis accumsan euismod, dolor ipsum bibendum enim, et varius turpis erat ut purus.",
|
||||
"~:count-unread-comments":1,
|
||||
"~:count-comments":1,
|
||||
"~:modified-at":"~m1718001240857",
|
||||
"~:page-id":"~udd5cc0bb-91ff-81b9-8004-77df9cd3edb2",
|
||||
"~:id": "~udd5cc0bb-91ff-81b9-8004-77df9cd3edb1",
|
||||
"~:file-name":"New File 3",
|
||||
"~:seqn":1,
|
||||
"~:is-resolved":false,
|
||||
"~:owner-id":"~u2e2da0fa-2d3e-81ec-8003-cb4453324510",
|
||||
"~:position":{
|
||||
"~#point":{
|
||||
"~:x":120.0,
|
||||
"~:y":120.0
|
||||
}
|
||||
},
|
||||
"~:frame-id": "~uec508673-9e3b-80bf-8004-77dfa30a2b13",
|
||||
"~:project-id": "~u0515a066-e303-8169-8004-73eb401b5d55",
|
||||
"~:created-at":"~m1718001240857"
|
||||
},
|
||||
{
|
||||
"~:page-name":"Page 1",
|
||||
"~:file-id":"~ud192fd06-a3e6-80d5-8004-7b7aaaea2a23",
|
||||
"~:participants":{
|
||||
"~#set":[
|
||||
"~u0515a066-e303-8169-8004-73eb4018f4e0"
|
||||
]
|
||||
},
|
||||
"~:content":"Duis lobortis ultricies lectus, in tristique tortor. Praesent mauris mi, finibus vel imperdiet quis, congue vel erat. Sed pharetra et ipsum at vestibulum.",
|
||||
"~:count-unread-comments":0,
|
||||
"~:count-comments":1,
|
||||
"~:modified-at":"~m1718001247587",
|
||||
"~:page-id":"~udd5cc0bb-91ff-81b9-8004-77df9cd3edb2",
|
||||
"~:id":"~ud192fd06-a3e6-80d5-8004-7b7ac25ac93a",
|
||||
"~:file-name":"New File 44",
|
||||
"~:seqn":2,
|
||||
"~:is-resolved":false,
|
||||
"~:owner-id":"~u2e2da0fa-2d3e-81ec-8003-cb4453324510",
|
||||
"~:position":{
|
||||
"~#point":{
|
||||
"~:x":160.0,
|
||||
"~:y":160.0
|
||||
}
|
||||
},
|
||||
"~:frame-id": "~uec508673-9e3b-80bf-8004-77dfa30a2b13",
|
||||
"~:project-id":"~u343837a3-0d75-808a-8004-659df7b7873e",
|
||||
"~:created-at":"~m1718001247587"
|
||||
}
|
||||
]
|
||||
@@ -1371,271 +1371,279 @@
|
||||
}
|
||||
],
|
||||
[
|
||||
"S-light",
|
||||
"G-LightDark",
|
||||
{
|
||||
"~#penpot/token-set": {
|
||||
"~:name": "light",
|
||||
"~:description": null,
|
||||
"~:modified-at": "~m1737542498290",
|
||||
"~:tokens": {
|
||||
"~#ordered-map": [
|
||||
[
|
||||
"accent.default",
|
||||
{
|
||||
"~#penpot/token": {
|
||||
"~:name": "accent.default",
|
||||
"~:type": "~:color",
|
||||
"~:value": "{colors.indigo.400}",
|
||||
"~:description": null,
|
||||
"~:modified-at": "~m1737542498290"
|
||||
}
|
||||
"~#ordered-map": [
|
||||
[
|
||||
"S-light",
|
||||
{
|
||||
"~#penpot/token-set": {
|
||||
"~:name": "LightDark/light",
|
||||
"~:description": null,
|
||||
"~:modified-at": "~m1737542498290",
|
||||
"~:tokens": {
|
||||
"~#ordered-map": [
|
||||
[
|
||||
"accent.default",
|
||||
{
|
||||
"~#penpot/token": {
|
||||
"~:name": "accent.default",
|
||||
"~:type": "~:color",
|
||||
"~:value": "{colors.indigo.400}",
|
||||
"~:description": null,
|
||||
"~:modified-at": "~m1737542498290"
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"bg.muted",
|
||||
{
|
||||
"~#penpot/token": {
|
||||
"~:name": "bg.muted",
|
||||
"~:type": "~:color",
|
||||
"~:value": "{colors.gray.100}",
|
||||
"~:description": null,
|
||||
"~:modified-at": "~m1737542498290"
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"bg.subtle",
|
||||
{
|
||||
"~#penpot/token": {
|
||||
"~:name": "bg.subtle",
|
||||
"~:type": "~:color",
|
||||
"~:value": "{colors.gray.200}",
|
||||
"~:description": null,
|
||||
"~:modified-at": "~m1737542498290"
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"accent.bg",
|
||||
{
|
||||
"~#penpot/token": {
|
||||
"~:name": "accent.bg",
|
||||
"~:type": "~:color",
|
||||
"~:value": "{colors.indigo.200}",
|
||||
"~:description": null,
|
||||
"~:modified-at": "~m1737542498290"
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"accent.onAccent",
|
||||
{
|
||||
"~#penpot/token": {
|
||||
"~:name": "accent.onAccent",
|
||||
"~:type": "~:color",
|
||||
"~:value": "{colors.white}",
|
||||
"~:description": null,
|
||||
"~:modified-at": "~m1737542498290"
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"shadows.default",
|
||||
{
|
||||
"~#penpot/token": {
|
||||
"~:name": "shadows.default",
|
||||
"~:type": "~:color",
|
||||
"~:value": "{colors.gray.900}",
|
||||
"~:description": null,
|
||||
"~:modified-at": "~m1737542498290"
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"fg.muted",
|
||||
{
|
||||
"~#penpot/token": {
|
||||
"~:name": "fg.muted",
|
||||
"~:type": "~:color",
|
||||
"~:value": "{colors.gray.700}",
|
||||
"~:description": null,
|
||||
"~:modified-at": "~m1737542498289"
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"fg.default",
|
||||
{
|
||||
"~#penpot/token": {
|
||||
"~:name": "fg.default",
|
||||
"~:type": "~:color",
|
||||
"~:value": "{colors.black}",
|
||||
"~:description": null,
|
||||
"~:modified-at": "~m1737542498289"
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"fg.subtle",
|
||||
{
|
||||
"~#penpot/token": {
|
||||
"~:name": "fg.subtle",
|
||||
"~:type": "~:color",
|
||||
"~:value": "{colors.gray.500}",
|
||||
"~:description": null,
|
||||
"~:modified-at": "~m1737542498290"
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"bg.default",
|
||||
{
|
||||
"~#penpot/token": {
|
||||
"~:name": "bg.default",
|
||||
"~:type": "~:color",
|
||||
"~:value": "{colors.white}",
|
||||
"~:description": null,
|
||||
"~:modified-at": "~m1737542498290"
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
],
|
||||
[
|
||||
"bg.muted",
|
||||
{
|
||||
"~#penpot/token": {
|
||||
"~:name": "bg.muted",
|
||||
"~:type": "~:color",
|
||||
"~:value": "{colors.gray.100}",
|
||||
"~:description": null,
|
||||
"~:modified-at": "~m1737542498290"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
[
|
||||
"S-dark",
|
||||
{
|
||||
"~#penpot/token-set": {
|
||||
"~:name": "LightDark/dark",
|
||||
"~:description": null,
|
||||
"~:modified-at": "~m1737542498291",
|
||||
"~:tokens": {
|
||||
"~#ordered-map": [
|
||||
[
|
||||
"accent.default",
|
||||
{
|
||||
"~#penpot/token": {
|
||||
"~:name": "accent.default",
|
||||
"~:type": "~:color",
|
||||
"~:value": "{colors.indigo.600}",
|
||||
"~:description": null,
|
||||
"~:modified-at": "~m1737542498291"
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"bg.muted",
|
||||
{
|
||||
"~#penpot/token": {
|
||||
"~:name": "bg.muted",
|
||||
"~:type": "~:color",
|
||||
"~:value": "{colors.gray.700}",
|
||||
"~:description": null,
|
||||
"~:modified-at": "~m1737542498291"
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"bg.subtle",
|
||||
{
|
||||
"~#penpot/token": {
|
||||
"~:name": "bg.subtle",
|
||||
"~:type": "~:color",
|
||||
"~:value": "{colors.gray.600}",
|
||||
"~:description": null,
|
||||
"~:modified-at": "~m1737542498291"
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"accent.bg",
|
||||
{
|
||||
"~#penpot/token": {
|
||||
"~:name": "accent.bg",
|
||||
"~:type": "~:color",
|
||||
"~:value": "{colors.indigo.800}",
|
||||
"~:description": null,
|
||||
"~:modified-at": "~m1737542498291"
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"accent.onAccent",
|
||||
{
|
||||
"~#penpot/token": {
|
||||
"~:name": "accent.onAccent",
|
||||
"~:type": "~:color",
|
||||
"~:value": "{colors.white}",
|
||||
"~:description": null,
|
||||
"~:modified-at": "~m1737542498291"
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"shadows.default",
|
||||
{
|
||||
"~#penpot/token": {
|
||||
"~:name": "shadows.default",
|
||||
"~:type": "~:color",
|
||||
"~:value": "rgba({colors.black}, 0.3)",
|
||||
"~:description": null,
|
||||
"~:modified-at": "~m1737542498291"
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"fg.muted",
|
||||
{
|
||||
"~#penpot/token": {
|
||||
"~:name": "fg.muted",
|
||||
"~:type": "~:color",
|
||||
"~:value": "{colors.gray.300}",
|
||||
"~:description": null,
|
||||
"~:modified-at": "~m1737542498291"
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"fg.default",
|
||||
{
|
||||
"~#penpot/token": {
|
||||
"~:name": "fg.default",
|
||||
"~:type": "~:color",
|
||||
"~:value": "{colors.white}",
|
||||
"~:description": null,
|
||||
"~:modified-at": "~m1737542498291"
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"fg.subtle",
|
||||
{
|
||||
"~#penpot/token": {
|
||||
"~:name": "fg.subtle",
|
||||
"~:type": "~:color",
|
||||
"~:value": "{colors.gray.500}",
|
||||
"~:description": null,
|
||||
"~:modified-at": "~m1737542498291"
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"bg.default",
|
||||
{
|
||||
"~#penpot/token": {
|
||||
"~:name": "bg.default",
|
||||
"~:type": "~:color",
|
||||
"~:value": "{colors.gray.900}",
|
||||
"~:description": null,
|
||||
"~:modified-at": "~m1737542498291"
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
],
|
||||
[
|
||||
"bg.subtle",
|
||||
{
|
||||
"~#penpot/token": {
|
||||
"~:name": "bg.subtle",
|
||||
"~:type": "~:color",
|
||||
"~:value": "{colors.gray.200}",
|
||||
"~:description": null,
|
||||
"~:modified-at": "~m1737542498290"
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"accent.bg",
|
||||
{
|
||||
"~#penpot/token": {
|
||||
"~:name": "accent.bg",
|
||||
"~:type": "~:color",
|
||||
"~:value": "{colors.indigo.200}",
|
||||
"~:description": null,
|
||||
"~:modified-at": "~m1737542498290"
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"accent.onAccent",
|
||||
{
|
||||
"~#penpot/token": {
|
||||
"~:name": "accent.onAccent",
|
||||
"~:type": "~:color",
|
||||
"~:value": "{colors.white}",
|
||||
"~:description": null,
|
||||
"~:modified-at": "~m1737542498290"
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"shadows.default",
|
||||
{
|
||||
"~#penpot/token": {
|
||||
"~:name": "shadows.default",
|
||||
"~:type": "~:color",
|
||||
"~:value": "{colors.gray.900}",
|
||||
"~:description": null,
|
||||
"~:modified-at": "~m1737542498290"
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"fg.muted",
|
||||
{
|
||||
"~#penpot/token": {
|
||||
"~:name": "fg.muted",
|
||||
"~:type": "~:color",
|
||||
"~:value": "{colors.gray.700}",
|
||||
"~:description": null,
|
||||
"~:modified-at": "~m1737542498289"
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"fg.default",
|
||||
{
|
||||
"~#penpot/token": {
|
||||
"~:name": "fg.default",
|
||||
"~:type": "~:color",
|
||||
"~:value": "{colors.black}",
|
||||
"~:description": null,
|
||||
"~:modified-at": "~m1737542498289"
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"fg.subtle",
|
||||
{
|
||||
"~#penpot/token": {
|
||||
"~:name": "fg.subtle",
|
||||
"~:type": "~:color",
|
||||
"~:value": "{colors.gray.500}",
|
||||
"~:description": null,
|
||||
"~:modified-at": "~m1737542498290"
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"bg.default",
|
||||
{
|
||||
"~#penpot/token": {
|
||||
"~:name": "bg.default",
|
||||
"~:type": "~:color",
|
||||
"~:value": "{colors.white}",
|
||||
"~:description": null,
|
||||
"~:modified-at": "~m1737542498290"
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"S-dark",
|
||||
{
|
||||
"~#penpot/token-set": {
|
||||
"~:name": "dark",
|
||||
"~:description": null,
|
||||
"~:modified-at": "~m1737542498291",
|
||||
"~:tokens": {
|
||||
"~#ordered-map": [
|
||||
[
|
||||
"accent.default",
|
||||
{
|
||||
"~#penpot/token": {
|
||||
"~:name": "accent.default",
|
||||
"~:type": "~:color",
|
||||
"~:value": "{colors.indigo.600}",
|
||||
"~:description": null,
|
||||
"~:modified-at": "~m1737542498291"
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"bg.muted",
|
||||
{
|
||||
"~#penpot/token": {
|
||||
"~:name": "bg.muted",
|
||||
"~:type": "~:color",
|
||||
"~:value": "{colors.gray.700}",
|
||||
"~:description": null,
|
||||
"~:modified-at": "~m1737542498291"
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"bg.subtle",
|
||||
{
|
||||
"~#penpot/token": {
|
||||
"~:name": "bg.subtle",
|
||||
"~:type": "~:color",
|
||||
"~:value": "{colors.gray.600}",
|
||||
"~:description": null,
|
||||
"~:modified-at": "~m1737542498291"
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"accent.bg",
|
||||
{
|
||||
"~#penpot/token": {
|
||||
"~:name": "accent.bg",
|
||||
"~:type": "~:color",
|
||||
"~:value": "{colors.indigo.800}",
|
||||
"~:description": null,
|
||||
"~:modified-at": "~m1737542498291"
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"accent.onAccent",
|
||||
{
|
||||
"~#penpot/token": {
|
||||
"~:name": "accent.onAccent",
|
||||
"~:type": "~:color",
|
||||
"~:value": "{colors.white}",
|
||||
"~:description": null,
|
||||
"~:modified-at": "~m1737542498291"
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"shadows.default",
|
||||
{
|
||||
"~#penpot/token": {
|
||||
"~:name": "shadows.default",
|
||||
"~:type": "~:color",
|
||||
"~:value": "rgba({colors.black}, 0.3)",
|
||||
"~:description": null,
|
||||
"~:modified-at": "~m1737542498291"
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"fg.muted",
|
||||
{
|
||||
"~#penpot/token": {
|
||||
"~:name": "fg.muted",
|
||||
"~:type": "~:color",
|
||||
"~:value": "{colors.gray.300}",
|
||||
"~:description": null,
|
||||
"~:modified-at": "~m1737542498291"
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"fg.default",
|
||||
{
|
||||
"~#penpot/token": {
|
||||
"~:name": "fg.default",
|
||||
"~:type": "~:color",
|
||||
"~:value": "{colors.white}",
|
||||
"~:description": null,
|
||||
"~:modified-at": "~m1737542498291"
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"fg.subtle",
|
||||
{
|
||||
"~#penpot/token": {
|
||||
"~:name": "fg.subtle",
|
||||
"~:type": "~:color",
|
||||
"~:value": "{colors.gray.500}",
|
||||
"~:description": null,
|
||||
"~:modified-at": "~m1737542498291"
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"bg.default",
|
||||
{
|
||||
"~#penpot/token": {
|
||||
"~:name": "bg.default",
|
||||
"~:type": "~:color",
|
||||
"~:value": "{colors.gray.900}",
|
||||
"~:description": null,
|
||||
"~:modified-at": "~m1737542498291"
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
],
|
||||
[
|
||||
@@ -1740,9 +1748,10 @@
|
||||
"~:group": "Core",
|
||||
"~:description": null,
|
||||
"~:is-source": false,
|
||||
"~:id": "core-light",
|
||||
"~:modified-at": "~m1737542746842",
|
||||
"~:sets": {
|
||||
"~#set": ["light", "theme", "core"]
|
||||
"~#set": ["LightDark/light", "theme", "core"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1755,9 +1764,10 @@
|
||||
"~:group": "Core",
|
||||
"~:description": null,
|
||||
"~:is-source": false,
|
||||
"~:id": "core-dark",
|
||||
"~:modified-at": "~m1737542746842",
|
||||
"~:sets": {
|
||||
"~#set": ["dark", "theme", "core"]
|
||||
"~#set": ["LightDark/dark", "theme", "core"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1777,9 +1787,10 @@
|
||||
"~:group": "",
|
||||
"~:description": null,
|
||||
"~:is-source": false,
|
||||
"~:id": "hidden-theme",
|
||||
"~:modified-at": "~m1737542683555",
|
||||
"~:sets": {
|
||||
"~#set": ["light", "theme", "core"]
|
||||
"~#set": ["LightDark/light", "theme", "core"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,6 +89,9 @@ export class WorkspacePage extends BaseWebSocketPage {
|
||||
this.tokensUpdateCreateModal = page.getByTestId(
|
||||
"token-update-create-modal",
|
||||
);
|
||||
this.tokenThemeUpdateCreateModal = page.getByTestId(
|
||||
"token-theme-update-create-modal",
|
||||
);
|
||||
this.tokenThemesSetsSidebar = page.getByTestId("token-themes-sets-sidebar");
|
||||
this.tokensSidebar = page.getByTestId("tokens-sidebar");
|
||||
this.tokenSetItems = page.getByTestId("tokens-set-item");
|
||||
@@ -96,6 +99,9 @@ export class WorkspacePage extends BaseWebSocketPage {
|
||||
this.tokenContextMenuForToken = page.getByTestId(
|
||||
"tokens-context-menu-for-token",
|
||||
);
|
||||
this.tokenContextMenuForSet = page.getByTestId(
|
||||
"tokens-context-menu-for-set",
|
||||
);
|
||||
}
|
||||
|
||||
async goToWorkspace({
|
||||
@@ -165,6 +171,25 @@ export class WorkspacePage extends BaseWebSocketPage {
|
||||
);
|
||||
}
|
||||
|
||||
async setupFileWithComments() {
|
||||
await this.mockRPC(
|
||||
"get-comment-threads?file-id=*",
|
||||
"workspace/get-comment-threads-unread.json",
|
||||
);
|
||||
await this.mockRPC(
|
||||
"get-file-fragment?file-id=*&fragment-id=*",
|
||||
"viewer/get-file-fragment-single-board.json",
|
||||
);
|
||||
await this.mockRPC(
|
||||
"get-comments?thread-id=*",
|
||||
"workspace/get-thread-comments.json",
|
||||
);
|
||||
await this.mockRPC(
|
||||
"update-comment-thread-status",
|
||||
"workspace/update-comment-thread-status.json",
|
||||
);
|
||||
}
|
||||
|
||||
async clickWithDragViewportAt(x, y, width, height) {
|
||||
await this.page.waitForTimeout(100);
|
||||
await this.viewport.hover({ position: { x, y } });
|
||||
@@ -255,4 +280,15 @@ export class WorkspacePage extends BaseWebSocketPage {
|
||||
async clickTogglePalettesVisibility(clickOptions = {}) {
|
||||
await this.togglePalettesVisibility.click(clickOptions);
|
||||
}
|
||||
|
||||
async openTokenThemesModal(clickOptions = {}) {
|
||||
await this.tokenThemesSetsSidebar.getByText("Edit").click(clickOptions);
|
||||
await expect(this.tokenThemeUpdateCreateModal).toBeVisible();
|
||||
}
|
||||
|
||||
async showComments(clickOptions = {}) {
|
||||
await this.page
|
||||
.getByRole("button", { name: "Comments (C)" })
|
||||
.click(clickOptions);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,3 +32,41 @@ test("Bug 9042 - Measurement unit dropdowns for columns are cut off in grid layo
|
||||
await rowsContainer.getByText("FR").nth(2).click();
|
||||
await expect(rowsContainer.getByText("%")).toBeInViewport();
|
||||
});
|
||||
|
||||
test("[Taiga #9116] Copy CSS background color in the selected format in the INSPECT tab", async ({
|
||||
page,
|
||||
context,
|
||||
}) => {
|
||||
const workspacePage = new WorkspacePage(page);
|
||||
await workspacePage.setupEmptyFile(page);
|
||||
await workspacePage.goToWorkspace();
|
||||
|
||||
await workspacePage.rectShapeButton.click();
|
||||
await workspacePage.clickWithDragViewportAt(128, 128, 200, 100);
|
||||
await workspacePage.clickLeafLayer("Rectangle");
|
||||
|
||||
const inspectButton = workspacePage.page.getByRole("tab", {
|
||||
name: "Inspect",
|
||||
});
|
||||
await inspectButton.click();
|
||||
|
||||
const colorDropdown = workspacePage.page
|
||||
.getByRole("combobox")
|
||||
.getByText("HEX");
|
||||
await colorDropdown.click();
|
||||
|
||||
const rgbaFormatButton = workspacePage.page.getByRole("option", {
|
||||
name: "RGBA",
|
||||
});
|
||||
await rgbaFormatButton.click();
|
||||
|
||||
const copyColorButton = workspacePage.page.getByRole("button", {
|
||||
name: "Copy color",
|
||||
});
|
||||
await copyColorButton.click();
|
||||
|
||||
const rgbaColorText = await page.evaluate(() =>
|
||||
navigator.clipboard.readText(),
|
||||
);
|
||||
expect(rgbaColorText).toContain("background: rgba(");
|
||||
});
|
||||
|
||||
@@ -15,6 +15,11 @@ const setupEmptyTokensFile = async (page) => {
|
||||
"workspace/get-team-tokens.json",
|
||||
);
|
||||
|
||||
await workspacePage.mockRPC(
|
||||
"update-file?id=*",
|
||||
"workspace/update-file-create-rect.json",
|
||||
);
|
||||
|
||||
await workspacePage.goToWorkspace();
|
||||
|
||||
const tokensTabButton = page.getByRole("tab", { name: "Tokens" });
|
||||
@@ -26,6 +31,7 @@ const setupEmptyTokensFile = async (page) => {
|
||||
tokenThemesSetsSidebar: workspacePage.tokenThemesSetsSidebar,
|
||||
tokenSetItems: workspacePage.tokenSetItems,
|
||||
tokenSetGroupItems: workspacePage.tokenSetGroupItems,
|
||||
tokenContextMenuForSet: workspacePage.tokenContextMenuForSet,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -41,6 +47,10 @@ const setupTokensFile = async (page) => {
|
||||
/get\-file\-fragment\?/,
|
||||
"workspace/get-file-fragment-tokens.json",
|
||||
);
|
||||
await workspacePage.mockRPC(
|
||||
"update-file?id=*",
|
||||
"workspace/update-file-create-rect.json",
|
||||
);
|
||||
|
||||
await workspacePage.goToWorkspace({
|
||||
fileId: "51e13852-1a8e-8037-8005-9e9413a1f1f6",
|
||||
@@ -53,11 +63,13 @@ const setupTokensFile = async (page) => {
|
||||
return {
|
||||
workspacePage,
|
||||
tokensUpdateCreateModal: workspacePage.tokensUpdateCreateModal,
|
||||
tokenThemeUpdateCreateModal: workspacePage.tokenThemeUpdateCreateModal,
|
||||
tokenThemesSetsSidebar: workspacePage.tokenThemesSetsSidebar,
|
||||
tokenSetItems: workspacePage.tokenSetItems,
|
||||
tokenSetGroupItems: workspacePage.tokenSetGroupItems,
|
||||
tokensSidebar: workspacePage.tokensSidebar,
|
||||
tokenContextMenuForToken: workspacePage.tokenContextMenuForToken,
|
||||
tokenContextMenuForSet: workspacePage.tokenContextMenuForSet,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -65,8 +77,7 @@ test.describe("Tokens: Tokens Tab", () => {
|
||||
test("Clicking tokens tab button opens tokens sidebar tab", async ({
|
||||
page,
|
||||
}) => {
|
||||
const { workspacePage, tokensUpdateCreateModal, tokenThemesSetsSidebar } =
|
||||
await setupEmptyTokensFile(page);
|
||||
await setupEmptyTokensFile(page);
|
||||
|
||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
||||
|
||||
@@ -77,7 +88,7 @@ test.describe("Tokens: Tokens Tab", () => {
|
||||
test("User creates color token and auto created set show up in the sidebar", async ({
|
||||
page,
|
||||
}) => {
|
||||
const { workspacePage, tokensUpdateCreateModal, tokenThemesSetsSidebar } =
|
||||
const { tokensUpdateCreateModal, tokenThemesSetsSidebar } =
|
||||
await setupEmptyTokensFile(page);
|
||||
|
||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
||||
@@ -93,7 +104,15 @@ test.describe("Tokens: Tokens Tab", () => {
|
||||
await nameField.click();
|
||||
await nameField.fill("color.primary");
|
||||
|
||||
// try invalid value
|
||||
await valueField.click();
|
||||
|
||||
await valueField.fill("1");
|
||||
await expect(
|
||||
tokensUpdateCreateModal.getByText("Invalid color value: 1"),
|
||||
).toBeVisible();
|
||||
|
||||
// valid value
|
||||
await valueField.fill("red");
|
||||
|
||||
const submitButton = tokensUpdateCreateModal.getByRole("button", {
|
||||
@@ -102,7 +121,11 @@ test.describe("Tokens: Tokens Tab", () => {
|
||||
await expect(submitButton).toBeEnabled();
|
||||
await submitButton.click();
|
||||
|
||||
await expect(tokensTabPanel.getByLabel("primary")).toBeEnabled();
|
||||
await expect(
|
||||
tokensTabPanel.getByRole("button", {
|
||||
name: "color.primary",
|
||||
}),
|
||||
).toBeEnabled();
|
||||
|
||||
// Create token referencing the previous one with keyboard
|
||||
|
||||
@@ -119,8 +142,11 @@ test.describe("Tokens: Tokens Tab", () => {
|
||||
await expect(submitButton).toBeEnabled();
|
||||
await nameField.press("Enter");
|
||||
|
||||
const referenceToken = tokensTabPanel.getByLabel("color.secondary");
|
||||
await expect(referenceToken).toBeEnabled();
|
||||
await expect(
|
||||
tokensTabPanel.getByRole("button", {
|
||||
name: "color.secondary",
|
||||
}),
|
||||
).toBeEnabled();
|
||||
|
||||
// Tokens tab panel should have two tokens with the color red / #ff0000
|
||||
await expect(tokensTabPanel.getByTitle("#ff0000")).toHaveCount(2);
|
||||
@@ -138,6 +164,60 @@ test.describe("Tokens: Tokens Tab", () => {
|
||||
).toHaveAttribute("aria-checked", "true");
|
||||
});
|
||||
|
||||
test("User creates dimensions token and auto created set show up in the sidebar", async ({
|
||||
page,
|
||||
}) => {
|
||||
const { tokensUpdateCreateModal, tokenThemesSetsSidebar } =
|
||||
await setupEmptyTokensFile(page);
|
||||
|
||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
||||
await tokensTabPanel.getByTitle("Add token: Dimensions").click();
|
||||
|
||||
await expect(tokensUpdateCreateModal).toBeVisible();
|
||||
|
||||
const nameField = tokensUpdateCreateModal.getByLabel("Name");
|
||||
const valueField = tokensUpdateCreateModal.getByLabel("Value");
|
||||
|
||||
await nameField.click();
|
||||
await nameField.fill("dimension.spacing.small");
|
||||
|
||||
// try invalid value first
|
||||
await valueField.click();
|
||||
|
||||
await valueField.fill("red");
|
||||
await expect(
|
||||
tokensUpdateCreateModal.getByText("Invalid token value: red"),
|
||||
).toBeVisible();
|
||||
|
||||
// valid value
|
||||
await valueField.fill("4px");
|
||||
await expect(
|
||||
tokensUpdateCreateModal.getByText("Resolved value: 4"),
|
||||
).toBeVisible();
|
||||
|
||||
const submitButton = tokensUpdateCreateModal.getByRole("button", {
|
||||
name: "Save",
|
||||
});
|
||||
await expect(submitButton).toBeEnabled();
|
||||
await submitButton.click();
|
||||
|
||||
await expect(
|
||||
tokensTabPanel.getByText("dimension.spacing.small"),
|
||||
).toBeVisible();
|
||||
|
||||
// Global set has been auto created and is active
|
||||
await expect(
|
||||
tokenThemesSetsSidebar.getByRole("button", {
|
||||
name: "Global",
|
||||
}),
|
||||
).toHaveCount(1);
|
||||
await expect(
|
||||
tokenThemesSetsSidebar.getByRole("button", {
|
||||
name: "Global",
|
||||
}),
|
||||
).toHaveAttribute("aria-checked", "true");
|
||||
});
|
||||
|
||||
test("User edits token and auto created set show up in the sidebar", async ({
|
||||
page,
|
||||
}) => {
|
||||
@@ -154,13 +234,13 @@ test.describe("Tokens: Tokens Tab", () => {
|
||||
const tokensColorGroup = tokensSidebar.getByRole("button", {
|
||||
name: "Color 92",
|
||||
});
|
||||
await expect(tokensColorGroup).toBeVisible;
|
||||
await expect(tokensColorGroup).toBeVisible();
|
||||
await tokensColorGroup.click();
|
||||
|
||||
const colorToken = tokensSidebar.getByRole("button", {
|
||||
name: "colors.blue.100",
|
||||
});
|
||||
await expect(colorToken).toBeVisible;
|
||||
await expect(colorToken).toBeVisible();
|
||||
await colorToken.click({ button: "right" });
|
||||
|
||||
await expect(tokenContextMenuForToken).toBeVisible();
|
||||
@@ -181,6 +261,87 @@ test.describe("Tokens: Tokens Tab", () => {
|
||||
await expect(colorTokenChanged).toBeVisible();
|
||||
});
|
||||
|
||||
test("User edits theme and activates it in the sidebar", async ({ page }) => {
|
||||
const { tokenThemesSetsSidebar, tokenThemeUpdateCreateModal } =
|
||||
await setupTokensFile(page);
|
||||
|
||||
await expect(tokenThemesSetsSidebar).toBeVisible();
|
||||
|
||||
await tokenThemesSetsSidebar.getByRole("button", { name: "Edit" }).click();
|
||||
|
||||
await expect(tokenThemeUpdateCreateModal).toBeVisible();
|
||||
await tokenThemeUpdateCreateModal
|
||||
.getByRole("button", { name: "sets" })
|
||||
.first()
|
||||
.click();
|
||||
|
||||
await tokenThemeUpdateCreateModal.getByLabel("Theme").fill("Changed");
|
||||
|
||||
const lightDarkSetGroup = tokenThemeUpdateCreateModal.getByRole("button", {
|
||||
name: "LightDark",
|
||||
exact: true,
|
||||
});
|
||||
await expect(lightDarkSetGroup).toBeVisible();
|
||||
const lightSet = tokenThemeUpdateCreateModal.getByRole("button", {
|
||||
name: "light",
|
||||
exact: true,
|
||||
});
|
||||
const darkSet = tokenThemeUpdateCreateModal.getByRole("button", {
|
||||
name: "dark",
|
||||
exact: true,
|
||||
});
|
||||
|
||||
// Mixed set group
|
||||
await expect(lightSet.getByRole("checkbox")).toBeChecked();
|
||||
await expect(darkSet.getByRole("checkbox")).not.toBeChecked();
|
||||
|
||||
// Disable all
|
||||
await lightDarkSetGroup.getByRole("checkbox").click();
|
||||
await expect(lightSet.getByRole("checkbox")).not.toBeChecked();
|
||||
await expect(darkSet.getByRole("checkbox")).not.toBeChecked();
|
||||
|
||||
// Enable all
|
||||
await lightDarkSetGroup.getByRole("checkbox").click();
|
||||
await expect(lightSet.getByRole("checkbox")).toBeChecked();
|
||||
await expect(darkSet.getByRole("checkbox")).toBeChecked();
|
||||
|
||||
await tokenThemeUpdateCreateModal
|
||||
.getByRole("button", {
|
||||
name: "save theme",
|
||||
})
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
tokenThemeUpdateCreateModal.getByText("Changed" + "4 active sets"),
|
||||
).toBeVisible();
|
||||
|
||||
await tokenThemeUpdateCreateModal
|
||||
.getByRole("button")
|
||||
.getByText("close")
|
||||
.click();
|
||||
await expect(tokenThemeUpdateCreateModal).not.toBeVisible();
|
||||
|
||||
const themeSelect = tokenThemesSetsSidebar.getByRole("combobox");
|
||||
|
||||
await themeSelect.click();
|
||||
await page
|
||||
.getByTestId("theme-select-dropdown")
|
||||
.getByRole("option", { name: "Changed" })
|
||||
.click();
|
||||
|
||||
const sidebarLightSet = tokenThemesSetsSidebar.getByRole("button", {
|
||||
name: "light",
|
||||
exact: true,
|
||||
});
|
||||
const sidebarDarkSet = tokenThemesSetsSidebar.getByRole("button", {
|
||||
name: "dark",
|
||||
exact: true,
|
||||
});
|
||||
|
||||
await expect(sidebarLightSet.getByRole("checkbox")).toBeChecked();
|
||||
await expect(sidebarDarkSet.getByRole("checkbox")).toBeChecked();
|
||||
});
|
||||
|
||||
test("User creates grouped color token", async ({ page }) => {
|
||||
const { workspacePage, tokensUpdateCreateModal, tokenThemesSetsSidebar } =
|
||||
await setupEmptyTokensFile(page);
|
||||
@@ -209,41 +370,368 @@ test.describe("Tokens: Tokens Tab", () => {
|
||||
|
||||
await expect(tokensTabPanel.getByLabel("color.dark.primary")).toBeEnabled();
|
||||
});
|
||||
|
||||
test("User duplicate color token", async ({ page }) => {
|
||||
const { tokensSidebar, tokenContextMenuForToken } =
|
||||
await setupTokensFile(page);
|
||||
|
||||
await expect(tokensSidebar).toBeVisible();
|
||||
|
||||
const tokensColorGroup = tokensSidebar.getByRole("button", {
|
||||
name: "Color 92",
|
||||
});
|
||||
|
||||
await expect(tokensColorGroup).toBeVisible();
|
||||
await tokensColorGroup.click();
|
||||
|
||||
const colorToken = tokensSidebar.getByRole("button", {
|
||||
name: "colors.blue.100",
|
||||
});
|
||||
|
||||
await colorToken.click({ button: "right" });
|
||||
await expect(tokenContextMenuForToken).toBeVisible();
|
||||
|
||||
await tokenContextMenuForToken.getByText("Duplicate token").click();
|
||||
await expect(tokenContextMenuForToken).not.toBeVisible();
|
||||
|
||||
await expect(
|
||||
tokensSidebar.getByRole("button", { name: "colors.blue.100-copy" }),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("User delete color token", async ({ page }) => {
|
||||
const { tokensSidebar, tokenContextMenuForToken } =
|
||||
await setupTokensFile(page);
|
||||
|
||||
await expect(tokensSidebar).toBeVisible();
|
||||
|
||||
const tokensColorGroup = tokensSidebar.getByRole("button", {
|
||||
name: "Color 92",
|
||||
});
|
||||
await expect(tokensColorGroup).toBeVisible();
|
||||
|
||||
await tokensColorGroup.click();
|
||||
|
||||
const colorToken = tokensSidebar.getByRole("button", {
|
||||
name: "colors.blue.100",
|
||||
});
|
||||
await expect(colorToken).toBeVisible();
|
||||
await colorToken.click({ button: "right" });
|
||||
|
||||
await expect(tokenContextMenuForToken).toBeVisible();
|
||||
await tokenContextMenuForToken.getByText("Delete token").click();
|
||||
|
||||
await expect(tokenContextMenuForToken).not.toBeVisible();
|
||||
await expect(colorToken).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("User fold/unfold color tokens", async ({ page }) => {
|
||||
const { tokensSidebar, tokenContextMenuForToken } =
|
||||
await setupTokensFile(page);
|
||||
|
||||
await expect(tokensSidebar).toBeVisible();
|
||||
|
||||
const tokensColorGroup = tokensSidebar.getByRole("button", {
|
||||
name: "Color 92",
|
||||
});
|
||||
await expect(tokensColorGroup).toBeVisible();
|
||||
await tokensColorGroup.click();
|
||||
|
||||
const colorToken = tokensSidebar.getByRole("button", {
|
||||
name: "colors.blue.100",
|
||||
});
|
||||
await expect(colorToken).toBeVisible();
|
||||
await tokensColorGroup.click();
|
||||
await expect(colorToken).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Tokens: Sets Tab", () => {
|
||||
const createSet = async (sidebar, setName, finalKey = "Enter") => {
|
||||
const tokensTabButton = sidebar
|
||||
.getByRole("button", { name: "Add set" })
|
||||
.click();
|
||||
|
||||
const changeSetInput = async (sidebar, setName, finalKey = "Enter") => {
|
||||
const setInput = sidebar.locator("input:focus");
|
||||
await expect(setInput).toBeVisible();
|
||||
await setInput.fill(setName);
|
||||
await setInput.press(finalKey);
|
||||
};
|
||||
|
||||
// test("User creates sets tree structure by entering a set path", async ({
|
||||
// page,
|
||||
// }) => {
|
||||
// const {
|
||||
// workspacePage,
|
||||
// tokenThemesSetsSidebar,
|
||||
// tokenSetItems,
|
||||
// tokenSetGroupItems,
|
||||
// } = await setupEmptyTokensFile(page);
|
||||
//
|
||||
// const tokensTabButton = tokenThemesSetsSidebar
|
||||
// .getByRole("button", { name: "Add set" })
|
||||
// .click();
|
||||
//
|
||||
// await createSet(tokenThemesSetsSidebar, "core/colors/light");
|
||||
// await createSet(tokenThemesSetsSidebar, "core/colors/dark");
|
||||
//
|
||||
// // User cancels during editing
|
||||
// await createSet(tokenThemesSetsSidebar, "core/colors/dark", "Escape");
|
||||
//
|
||||
// await expect(tokenSetItems).toHaveCount(2);
|
||||
// await expect(tokenSetGroupItems).toHaveCount(2);
|
||||
// });
|
||||
const createSet = async (sidebar, setName, finalKey = "Enter") => {
|
||||
const tokensTabButton = sidebar
|
||||
.getByRole("button", { name: "Add set" })
|
||||
.click();
|
||||
|
||||
await changeSetInput(sidebar, setName, (finalKey = "Enter"));
|
||||
};
|
||||
|
||||
const assertSetsList = async (el, sets) => {
|
||||
const buttons = await el.getByRole("button").allTextContents();
|
||||
const filteredButtons = buttons.filter(
|
||||
(text) => text && text !== "Create one.",
|
||||
);
|
||||
await expect(filteredButtons).toEqual(sets);
|
||||
};
|
||||
|
||||
test("User creates sets tree structure by entering a set path", async ({
|
||||
page,
|
||||
}) => {
|
||||
const { tokenThemesSetsSidebar, tokenContextMenuForSet } =
|
||||
await setupEmptyTokensFile(page);
|
||||
|
||||
const tokensTabButton = tokenThemesSetsSidebar
|
||||
.getByRole("button", { name: "Add set" })
|
||||
.click();
|
||||
|
||||
await createSet(tokenThemesSetsSidebar, "core/colors/light");
|
||||
await createSet(tokenThemesSetsSidebar, "core/colors/dark");
|
||||
|
||||
await assertSetsList(tokenThemesSetsSidebar, [
|
||||
"core",
|
||||
"colors",
|
||||
"light",
|
||||
"dark",
|
||||
]);
|
||||
|
||||
// User renames set
|
||||
await tokenThemesSetsSidebar
|
||||
.getByRole("button", { name: "light" })
|
||||
.click({ button: "right" });
|
||||
await expect(tokenContextMenuForSet).toBeVisible();
|
||||
await tokenContextMenuForSet.getByText("Rename").click();
|
||||
await changeSetInput(tokenThemesSetsSidebar, "light-renamed");
|
||||
|
||||
// User cancels during editing
|
||||
await createSet(tokenThemesSetsSidebar, "core/colors/dark", "Escape");
|
||||
|
||||
await assertSetsList(tokenThemesSetsSidebar, [
|
||||
"core",
|
||||
"colors",
|
||||
"light-renamed",
|
||||
"dark",
|
||||
]);
|
||||
|
||||
// Creates nesting by renaming set with double click
|
||||
await tokenThemesSetsSidebar
|
||||
.getByRole("button", { name: "light-renamed" })
|
||||
.click({ button: "right" });
|
||||
await expect(tokenContextMenuForSet).toBeVisible();
|
||||
await tokenContextMenuForSet.getByText("Rename").click();
|
||||
await changeSetInput(tokenThemesSetsSidebar, "nested/light");
|
||||
|
||||
await assertSetsList(tokenThemesSetsSidebar, [
|
||||
"core",
|
||||
"colors",
|
||||
"nested",
|
||||
"light",
|
||||
"dark",
|
||||
]);
|
||||
|
||||
// Create set in group
|
||||
await tokenThemesSetsSidebar
|
||||
.getByRole("button", { name: "core" })
|
||||
.click({ button: "right" });
|
||||
await expect(tokenContextMenuForSet).toBeVisible();
|
||||
await tokenContextMenuForSet.getByText("Add set to this group").click();
|
||||
await changeSetInput(tokenThemesSetsSidebar, "sizes/small");
|
||||
|
||||
await assertSetsList(tokenThemesSetsSidebar, [
|
||||
"core",
|
||||
"colors",
|
||||
"nested",
|
||||
"light",
|
||||
"dark",
|
||||
"sizes",
|
||||
"small",
|
||||
]);
|
||||
|
||||
// User deletes set
|
||||
await tokenThemesSetsSidebar
|
||||
.getByRole("button", { name: "nested" })
|
||||
.click({ button: "right" });
|
||||
await expect(tokenContextMenuForSet).toBeVisible();
|
||||
await tokenContextMenuForSet.getByText("Delete").click();
|
||||
|
||||
await assertSetsList(tokenThemesSetsSidebar, [
|
||||
"core",
|
||||
"colors",
|
||||
"dark",
|
||||
"sizes",
|
||||
"small",
|
||||
]);
|
||||
});
|
||||
|
||||
test("Fold/Unfold set", async ({ page }) => {
|
||||
const { tokenThemesSetsSidebar, tokenSetGroupItems } =
|
||||
await setupTokensFile(page);
|
||||
|
||||
await expect(tokenThemesSetsSidebar).toBeVisible();
|
||||
|
||||
const darkSet = tokenThemesSetsSidebar.getByRole("button", {
|
||||
name: "dark",
|
||||
exact: true,
|
||||
});
|
||||
|
||||
await expect(darkSet).toBeVisible();
|
||||
|
||||
const setGroup = await tokenSetGroupItems
|
||||
.filter({ hasText: "LightDark" })
|
||||
.first();
|
||||
|
||||
await setGroup.getByRole("button").filter({ title: "Collapse" }).click();
|
||||
|
||||
await expect(darkSet).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("Change current theme", async ({ page }) => {
|
||||
const { tokenThemesSetsSidebar, tokenSetItems } =
|
||||
await setupTokensFile(page);
|
||||
|
||||
await expect(tokenSetItems.nth(1)).toHaveAttribute("aria-checked", "true");
|
||||
await expect(tokenSetItems.nth(2)).toHaveAttribute("aria-checked", "false");
|
||||
|
||||
await tokenThemesSetsSidebar.getByTestId("theme-select").click();
|
||||
await page
|
||||
.getByTestId("theme-select-dropdown")
|
||||
.getByRole("option", { name: "Dark", exact: true })
|
||||
.click();
|
||||
|
||||
await expect(tokenSetItems.nth(1)).toHaveAttribute("aria-checked", "false");
|
||||
await expect(tokenSetItems.nth(2)).toHaveAttribute("aria-checked", "true");
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Tokens: Themes modal", () => {
|
||||
test("Delete theme", async ({ page }) => {
|
||||
const { tokenThemeUpdateCreateModal, workspacePage } =
|
||||
await setupTokensFile(page);
|
||||
|
||||
workspacePage.openTokenThemesModal();
|
||||
|
||||
await expect(
|
||||
tokenThemeUpdateCreateModal.getByRole("button", { name: "Delete theme" }),
|
||||
).toHaveCount(2);
|
||||
|
||||
await tokenThemeUpdateCreateModal
|
||||
.getByRole("button", { name: "Delete theme" })
|
||||
.first()
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
tokenThemeUpdateCreateModal.getByRole("button", { name: "Delete theme" }),
|
||||
).toHaveCount(1);
|
||||
});
|
||||
|
||||
test("Add new theme", async ({ page }) => {
|
||||
const { tokenThemeUpdateCreateModal, workspacePage } =
|
||||
await setupTokensFile(page);
|
||||
|
||||
workspacePage.openTokenThemesModal();
|
||||
|
||||
await tokenThemeUpdateCreateModal
|
||||
.getByRole("button", {
|
||||
name: "Add new theme",
|
||||
})
|
||||
.click();
|
||||
|
||||
await tokenThemeUpdateCreateModal
|
||||
.getByLabel("Group")
|
||||
.fill("New Group name");
|
||||
await tokenThemeUpdateCreateModal
|
||||
.getByLabel("Theme")
|
||||
.fill("New Theme name");
|
||||
|
||||
await tokenThemeUpdateCreateModal
|
||||
.getByRole("button", {
|
||||
name: "Save theme",
|
||||
})
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
tokenThemeUpdateCreateModal.getByText("New Theme name"),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
tokenThemeUpdateCreateModal.getByText("New Group name"),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("Edit theme", async ({ page }) => {
|
||||
const { tokenThemeUpdateCreateModal, workspacePage } =
|
||||
await setupTokensFile(page);
|
||||
|
||||
workspacePage.openTokenThemesModal();
|
||||
|
||||
await expect(
|
||||
tokenThemeUpdateCreateModal.getByText("no sets"),
|
||||
).not.toBeVisible();
|
||||
await expect(
|
||||
tokenThemeUpdateCreateModal.getByText("3 active sets"),
|
||||
).toHaveCount(2);
|
||||
|
||||
await tokenThemeUpdateCreateModal
|
||||
.getByText("3 active sets")
|
||||
.first()
|
||||
.click();
|
||||
|
||||
await tokenThemeUpdateCreateModal
|
||||
.getByLabel("Theme")
|
||||
.fill("Changed Theme name");
|
||||
await tokenThemeUpdateCreateModal
|
||||
.getByLabel("Group")
|
||||
.fill("Changed Group name");
|
||||
|
||||
const checkboxes = await tokenThemeUpdateCreateModal
|
||||
.locator('[role="checkbox"]')
|
||||
.all();
|
||||
|
||||
for (const checkbox of checkboxes) {
|
||||
const isChecked = await checkbox.getAttribute("aria-checked");
|
||||
|
||||
if (isChecked === "true") {
|
||||
await checkbox.click();
|
||||
}
|
||||
}
|
||||
|
||||
await tokenThemeUpdateCreateModal
|
||||
.getByRole("button", {
|
||||
name: "Save theme",
|
||||
})
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
tokenThemeUpdateCreateModal.getByText("Changed Theme name"),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
tokenThemeUpdateCreateModal.getByText("Changed Group name"),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test.describe("Tokens: Apply token", () => {
|
||||
test("User applies color token to a shape", async ({ page }) => {
|
||||
const { workspacePage, tokensSidebar, tokenContextMenuForToken } =
|
||||
await setupTokensFile(page);
|
||||
|
||||
await page.getByRole("tab", { name: "Layers" }).click();
|
||||
|
||||
await workspacePage.layers
|
||||
.getByTestId("layer-row")
|
||||
.filter({ hasText: "Button" })
|
||||
.click();
|
||||
|
||||
const tokensTabButton = page.getByRole("tab", { name: "Tokens" });
|
||||
await tokensTabButton.click();
|
||||
|
||||
await tokensSidebar
|
||||
.getByRole("button")
|
||||
.filter({ hasText: "Color" })
|
||||
.click();
|
||||
|
||||
await tokensSidebar
|
||||
.getByRole("button", { name: "colors.black" })
|
||||
.click({ button: "right" });
|
||||
await tokenContextMenuForToken.getByText("Fill").click();
|
||||
const inputColor = await workspacePage.page
|
||||
.getByPlaceholder("Mixed")
|
||||
.nth(2);
|
||||
|
||||
await expect(inputColor).toHaveValue("000000");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
37
frontend/playwright/ui/specs/workspace-comments.spec.js
Normal file
37
frontend/playwright/ui/specs/workspace-comments.spec.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { WorkspacePage } from "../pages/WorkspacePage";
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await WorkspacePage.init(page);
|
||||
});
|
||||
|
||||
test("Group bubbles when zooming out if they overlap", async ({ page }) => {
|
||||
const workspacePage = new WorkspacePage(page);
|
||||
await workspacePage.setupEmptyFile();
|
||||
|
||||
await workspacePage.setupFileWithComments();
|
||||
|
||||
await workspacePage.goToWorkspace();
|
||||
|
||||
await workspacePage.showComments();
|
||||
|
||||
await expect(page.getByTestId("floating-thread-bubble-1")).toBeVisible();
|
||||
await expect(page.getByTestId("floating-thread-bubble-2")).toBeVisible();
|
||||
await expect(page.getByTestId("floating-thread-bubble-1-2")).toBeHidden();
|
||||
|
||||
const zoom = page.getByTitle("Zoom");
|
||||
await zoom.click();
|
||||
|
||||
const zoomOut = page.getByTitle("Zoom out");
|
||||
await zoomOut.click();
|
||||
await zoomOut.click();
|
||||
await zoomOut.click();
|
||||
await zoomOut.click();
|
||||
|
||||
await expect(page.getByTestId("floating-thread-bubble-1")).toBeHidden();
|
||||
await expect(page.getByTestId("floating-thread-bubble-2")).toBeHidden();
|
||||
await expect(page.getByTestId("floating-thread-bubble-1-2")).toBeVisible();
|
||||
await expect(page.getByTestId("floating-thread-bubble-1-2")).toHaveClass(
|
||||
/unread/,
|
||||
);
|
||||
});
|
||||
3
frontend/resources/images/icons/variant.svg
Normal file
3
frontend/resources/images/icons/variant.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M8.75 1.25a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Zm-1.5 13.5a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0ZM8.75 8a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0ZM5.375 4.625a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Zm0 6.75a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Zm5.25 0a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0Zm0-6.75a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0ZM1.25 8.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm13.5-1.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 558 B |
@@ -873,7 +873,7 @@
|
||||
top: $s-32;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
max-height: $s-300;
|
||||
max-height: var(--menu-max-height, $s-300);
|
||||
padding: $s-2;
|
||||
margin: 0;
|
||||
margin-top: $s-1;
|
||||
|
||||
@@ -127,6 +127,7 @@
|
||||
--layer-row-foreground-color-selected: var(--color-accent-primary);
|
||||
--layer-row-foreground-color-drag: var(--color-accent-tertiary);
|
||||
--layer-row-foreground-color-focus: var(--color-foreground-primary);
|
||||
--layer-row-foreground-color-placeholder: var(--color-foreground-secondary);
|
||||
--layer-row-border-color-focus: var(--color-accent-primary);
|
||||
--layer-child-row-background-color: var(--color-background-tertiary);
|
||||
--layer-child-row-foreground-color: var(--color-foreground-secondary);
|
||||
|
||||
@@ -14,8 +14,10 @@
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.data.event :as ev]
|
||||
[app.main.data.helpers :as dsh]
|
||||
[app.main.data.notifications :as ntf]
|
||||
[app.main.data.team :as dtm]
|
||||
[app.main.repo :as rp]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[beicon.v2.core :as rx]
|
||||
[potok.v2.core :as ptk]))
|
||||
|
||||
@@ -452,6 +454,26 @@
|
||||
(rx/map #(partial fetched-users %))))))
|
||||
(rx/catch #(rx/throw {:type :comment-error})))))))
|
||||
|
||||
(defn mark-all-threads-as-read
|
||||
"Mark all threads as read"
|
||||
[team-id]
|
||||
(ptk/reify ::mark-all-threads-as-read
|
||||
ev/Event
|
||||
(-data [_] {})
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [threads (-> state :comment-threads vals)]
|
||||
(rx/concat
|
||||
(->> (rp/cmd! :mark-all-threads-as-read {:threads (mapv :id threads)})
|
||||
(rx/map #(retrieve-unread-comment-threads team-id))
|
||||
(rx/catch #(rx/throw {:type :comment-error})))
|
||||
(rx/of (ntf/show {:level :info
|
||||
:type :toast
|
||||
:content (tr "dashboard.mark-all-as-read.success")
|
||||
:timeout 7000})))))))
|
||||
|
||||
|
||||
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Local State
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
[app.util.time :as dt]
|
||||
[beicon.v2.core :as rx]
|
||||
[clojure.set :as set]
|
||||
[cuerdas.core :as str]
|
||||
[potok.v2.core :as ptk]))
|
||||
|
||||
(log/set-level! :warn)
|
||||
@@ -38,7 +39,9 @@
|
||||
(declare process-message)
|
||||
|
||||
(defn initialize
|
||||
[]
|
||||
[team-id]
|
||||
(assert (uuid? team-id) "expected uuid instance for `team-id`")
|
||||
|
||||
(ptk/reify ::initialize
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
@@ -46,8 +49,8 @@
|
||||
profile-id (:profile-id state)]
|
||||
|
||||
(->> (rx/merge
|
||||
(rx/of (fetch-projects)
|
||||
(df/fetch-fonts))
|
||||
(rx/of (fetch-projects team-id)
|
||||
(df/fetch-fonts team-id))
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? ::dws/message))
|
||||
(rx/map deref)
|
||||
@@ -60,8 +63,8 @@
|
||||
(rx/take-until stopper))))))
|
||||
|
||||
(defn finalize
|
||||
[]
|
||||
(ptk/data-event ::finalize {}))
|
||||
[team-id]
|
||||
(ptk/data-event ::finalize {:team-id team-id}))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Data Fetching (context aware: current team)
|
||||
@@ -69,7 +72,7 @@
|
||||
|
||||
;; --- EVENT: fetch-projects
|
||||
|
||||
(defn projects-fetched
|
||||
(defn- projects-fetched
|
||||
[projects]
|
||||
(ptk/reify ::projects-fetched
|
||||
ptk/UpdateEvent
|
||||
@@ -80,13 +83,12 @@
|
||||
projects))))
|
||||
|
||||
(defn fetch-projects
|
||||
[]
|
||||
[team-id]
|
||||
(ptk/reify ::fetch-projects
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [team-id (:current-team-id state)]
|
||||
(->> (rp/cmd! :get-projects {:team-id team-id})
|
||||
(rx/map projects-fetched))))))
|
||||
(watch [_ _ _]
|
||||
(->> (rp/cmd! :get-projects {:team-id team-id})
|
||||
(rx/map projects-fetched)))))
|
||||
|
||||
;; --- EVENT: search
|
||||
|
||||
@@ -115,7 +117,7 @@
|
||||
|
||||
;; --- EVENT: recent-files
|
||||
|
||||
(defn recent-files-fetched
|
||||
(defn- recent-files-fetched
|
||||
[files]
|
||||
(ptk/reify ::recent-files-fetched
|
||||
ptk/UpdateEvent
|
||||
@@ -126,13 +128,14 @@
|
||||
(update :files d/merge files))))))
|
||||
|
||||
(defn fetch-recent-files
|
||||
[]
|
||||
(ptk/reify ::fetch-recent-files
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [team-id (:current-team-id state)]
|
||||
(->> (rp/cmd! :get-team-recent-files {:team-id team-id})
|
||||
(rx/map recent-files-fetched))))))
|
||||
([] (fetch-recent-files nil))
|
||||
([team-id]
|
||||
(ptk/reify ::fetch-recent-files
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(when-let [team-id (or team-id (:current-team-id state))]
|
||||
(->> (rp/cmd! :get-team-recent-files {:team-id team-id})
|
||||
(rx/map recent-files-fetched)))))))
|
||||
|
||||
;; --- EVENT: fetch-template-files
|
||||
|
||||
@@ -248,15 +251,18 @@
|
||||
(ptk/reify ::create-project
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [team-id (:current-team-id state)
|
||||
projects (dsh/lookup-team-projects state team-id)
|
||||
unames (cfh/get-used-names projects)
|
||||
name (cfh/generate-unique-name unames (str (tr "dashboard.new-project-prefix") " 1"))
|
||||
params {:name name
|
||||
:team-id team-id}
|
||||
(let [team-id (:current-team-id state)
|
||||
projects (dsh/lookup-team-projects state team-id)
|
||||
unames (cfh/get-used-names projects)
|
||||
base-name (tr "dashboard.new-project-prefix")
|
||||
name (cfh/generate-unique-name base-name unames :immediate-suffix? true)
|
||||
team-id (:current-team-id state)
|
||||
params {:name name
|
||||
:team-id team-id}
|
||||
{:keys [on-success on-error]
|
||||
:or {on-success identity
|
||||
on-error rx/throw}} (meta params)]
|
||||
on-error rx/throw}}
|
||||
(meta params)]
|
||||
(->> (rp/cmd! :create-project params)
|
||||
(rx/tap on-success)
|
||||
(rx/map project-created)
|
||||
@@ -281,13 +287,18 @@
|
||||
:name name})
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(watch [_ state _]
|
||||
(let [{:keys [on-success on-error]
|
||||
:or {on-success identity
|
||||
on-error rx/throw}} (meta params)
|
||||
|
||||
new-name (str name " " (tr "dashboard.copy-suffix"))]
|
||||
|
||||
projects (get state :projects)
|
||||
unames (cfh/get-used-names projects)
|
||||
suffix-fn (fn [copy-count]
|
||||
(str/concat " "
|
||||
(tr "dashboard.copy-suffix")
|
||||
(when (> copy-count 1)
|
||||
(str " " copy-count))))
|
||||
new-name (cfh/generate-unique-name name unames :suffix-fn suffix-fn)]
|
||||
(->> (rp/cmd! :duplicate-project {:project-id id :name new-name})
|
||||
(rx/tap on-success)
|
||||
(rx/map project-duplicated)
|
||||
@@ -478,16 +489,19 @@
|
||||
(watch [it state _]
|
||||
(let [{:keys [on-success on-error]
|
||||
:or {on-success identity
|
||||
on-error rx/throw}} (meta params)
|
||||
on-error rx/throw}}
|
||||
(meta params)
|
||||
|
||||
files (dsh/lookup-team-files state)
|
||||
unames (cfh/get-used-names files)
|
||||
name (or name (cfh/generate-unique-name unames (str (tr "dashboard.new-file-prefix") " 1")))
|
||||
features (-> (features/get-team-enabled-features state)
|
||||
(set/difference cfeat/frontend-only-features))
|
||||
params (-> params
|
||||
(assoc :name name)
|
||||
(assoc :features features))]
|
||||
files (dsh/lookup-team-files state)
|
||||
unames (cfh/get-used-names files)
|
||||
base-name (tr "dashboard.new-file-prefix")
|
||||
name (or name
|
||||
(cfh/generate-unique-name base-name unames :immediate-suffix? true))
|
||||
features (-> (features/get-team-enabled-features state)
|
||||
(set/difference cfeat/frontend-only-features))
|
||||
params (-> params
|
||||
(assoc :name name)
|
||||
(assoc :features features))]
|
||||
|
||||
(->> (rp/cmd! :create-file params)
|
||||
(rx/tap on-success)
|
||||
@@ -502,13 +516,17 @@
|
||||
(dm/assert! (string? name))
|
||||
(ptk/reify ::duplicate-file
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(watch [_ state _]
|
||||
(let [{:keys [on-success on-error]
|
||||
:or {on-success identity
|
||||
on-error rx/throw}} (meta params)
|
||||
|
||||
new-name (str name " " (tr "dashboard.copy-suffix"))]
|
||||
|
||||
unames (cfh/get-used-names (get state :files))
|
||||
suffix-fn (fn [copy-count]
|
||||
(str/concat " "
|
||||
(tr "dashboard.copy-suffix")
|
||||
(when (> copy-count 1)
|
||||
(str " " copy-count))))
|
||||
new-name (cfh/generate-unique-name name unames :suffix-fn suffix-fn)]
|
||||
(->> (rp/cmd! :duplicate-file {:file-id id :name new-name})
|
||||
(rx/tap on-success)
|
||||
(rx/map file-created)
|
||||
@@ -591,10 +609,10 @@
|
||||
name (if in-project?
|
||||
(let [files (dsh/lookup-team-files state team-id)
|
||||
unames (cfh/get-used-names files)]
|
||||
(cfh/generate-unique-name unames (str (tr "dashboard.new-file-prefix") " 1")))
|
||||
(cfh/generate-unique-name (tr "dashboard.new-file-prefix") unames :immediate-suffix? true))
|
||||
(let [projects (dsh/lookup-team-projects state team-id)
|
||||
unames (cfh/get-used-names projects)]
|
||||
(cfh/generate-unique-name unames (str (tr "dashboard.new-project-prefix") " 1"))))
|
||||
(cfh/generate-unique-name (tr "dashboard.new-project-prefix") unames :immediate-suffix? true)))
|
||||
params (if in-project?
|
||||
{:project-id (:project-id pparams)
|
||||
:name name}
|
||||
|
||||
@@ -266,10 +266,10 @@
|
||||
(defn export-shapes-event
|
||||
[exports origin]
|
||||
(let [types (reduce (fn [counts {:keys [type]}]
|
||||
(if (#{:png :pdf :svg :jpeg} type)
|
||||
(if (#{:png :jpeg :webp :svg :pdf} type)
|
||||
(update counts type inc)
|
||||
counts))
|
||||
{:png 0, :pdf 0, :svg 0, :jpeg 0}
|
||||
{:png 0, :jpeg 0, :webp 0, :pdf 0, :svg 0}
|
||||
exports)]
|
||||
(ptk/event
|
||||
::ev/event (merge types
|
||||
|
||||
@@ -73,13 +73,12 @@
|
||||
(fonts/register! :custom fonts))))))
|
||||
|
||||
(defn fetch-fonts
|
||||
[]
|
||||
(ptk/reify ::load-team-fonts
|
||||
[team-id]
|
||||
(ptk/reify ::fetch-fonts
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [team-id (:current-team-id state)]
|
||||
(->> (rp/cmd! :get-font-variants {:team-id team-id})
|
||||
(rx/map fonts-fetched))))))
|
||||
(watch [_ _ _]
|
||||
(->> (rp/cmd! :get-font-variants {:team-id team-id})
|
||||
(rx/map fonts-fetched)))))
|
||||
|
||||
(defn process-upload
|
||||
"Given a seq of blobs and the team id, creates a ready-to-use fonts
|
||||
|
||||
@@ -64,13 +64,14 @@
|
||||
(update :profiles merge (d/index-by :id members))))))
|
||||
|
||||
(defn fetch-members
|
||||
[]
|
||||
(ptk/reify ::fetch-members
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [team-id (:current-team-id state)]
|
||||
(->> (rp/cmd! :get-team-members {:team-id team-id})
|
||||
(rx/map (partial members-fetched team-id)))))))
|
||||
([] (fetch-members nil))
|
||||
([team-id]
|
||||
(ptk/reify ::fetch-members
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(when-let [team-id (or team-id (:current-team-id state))]
|
||||
(->> (rp/cmd! :get-team-members {:team-id team-id})
|
||||
(rx/map (partial members-fetched team-id))))))))
|
||||
|
||||
(defn- invitations-fetched
|
||||
[team-id invitations]
|
||||
@@ -88,41 +89,24 @@
|
||||
(->> (rp/cmd! :get-team-invitations {:team-id team-id})
|
||||
(rx/map (partial invitations-fetched team-id)))))))
|
||||
|
||||
(defn set-current-team
|
||||
[{:keys [id permissions features] :as team}]
|
||||
(ptk/reify ::set-current-team
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
;; FIXME: redundant operation, only necessary on workspace
|
||||
;; until workspace initialization is refactored
|
||||
(update-in [:teams id] merge team)
|
||||
(assoc :permissions permissions)
|
||||
;; FIXME: this is a redundant operation that only needed by
|
||||
;; workspace; ti will not be needed after workspace
|
||||
;; bootstrap & urls refactor
|
||||
(assoc :current-team-id id)))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(rx/of (features/initialize (or features #{}))))
|
||||
|
||||
ptk/EffectEvent
|
||||
(effect [_ _ _]
|
||||
(swap! storage/global assoc ::current-team-id id))))
|
||||
|
||||
(defn- team-initialized
|
||||
[]
|
||||
[team-id]
|
||||
(ptk/reify ::team-initialized
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [team-id (:current-team-id state)
|
||||
teams (get state :teams)
|
||||
team (get teams team-id)]
|
||||
(let [teams (get state :teams)
|
||||
team (get teams team-id)]
|
||||
(if (not team)
|
||||
(rx/throw (ex/error :type :authentication))
|
||||
(rx/of (set-current-team team)
|
||||
(fetch-members)))))))
|
||||
(let [permissions (get team :permissions)
|
||||
features (get team :features)]
|
||||
(rx/of #(assoc % :permissions permissions)
|
||||
(features/initialize (or features #{}))
|
||||
(fetch-members team-id))))))
|
||||
|
||||
ptk/EffectEvent
|
||||
(effect [_ _ _]
|
||||
(swap! storage/global assoc ::current-team-id team-id))))
|
||||
|
||||
(defn initialize-team
|
||||
[team-id]
|
||||
@@ -138,8 +122,7 @@
|
||||
(rx/of (fetch-teams))
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? ::teams-fetched))
|
||||
(rx/observe-on :async)
|
||||
(rx/map team-initialized)))
|
||||
(rx/map (partial team-initialized team-id))))
|
||||
(rx/take-until stopper))))))
|
||||
|
||||
(defn finalize-team
|
||||
@@ -169,7 +152,7 @@
|
||||
params (assoc params :team-id team-id)]
|
||||
(->> (rp/cmd! :update-team-member-role params)
|
||||
(rx/mapcat (fn [_]
|
||||
(rx/of (fetch-members)
|
||||
(rx/of (fetch-members team-id)
|
||||
(fetch-teams)
|
||||
(ptk/data-event ::ev/event
|
||||
{::ev/name "update-team-member-role"
|
||||
@@ -187,7 +170,7 @@
|
||||
params (assoc params :team-id team-id)]
|
||||
(->> (rp/cmd! :delete-team-member params)
|
||||
(rx/mapcat (fn [_]
|
||||
(rx/of (fetch-members)
|
||||
(rx/of (fetch-members team-id)
|
||||
(fetch-teams)
|
||||
(ptk/data-event ::ev/event
|
||||
{::ev/name "delete-team-member"
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.files.changes-builder :as pcb]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.logic.tokens :as clt]
|
||||
[app.common.types.shape :as cts]
|
||||
@@ -17,14 +18,33 @@
|
||||
[app.main.data.helpers :as dsh]
|
||||
[app.main.data.notifications :as ntf]
|
||||
[app.main.data.workspace.shapes :as dwsh]
|
||||
[app.main.data.workspace.tokens.selected-set :as dwts]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.workspace.tokens.update :as wtu]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[beicon.v2.core :as rx]
|
||||
[cuerdas.core :as str]
|
||||
[potok.v2.core :as ptk]))
|
||||
|
||||
(declare set-selected-token-set-name)
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; TOKENS Getters
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
;; FIXME: lookup rename
|
||||
|
||||
(defn get-tokens-lib
|
||||
[state]
|
||||
(-> (dsh/lookup-file-data state)
|
||||
(get :tokens-lib)))
|
||||
|
||||
(defn lookup-token-set
|
||||
([state]
|
||||
(when-let [selected (dm/get-in state [:workspace-tokens :selected-token-set-name])]
|
||||
(lookup-token-set state selected)))
|
||||
([state name]
|
||||
(some-> (get-tokens-lib state)
|
||||
(ctob/get-set name))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Helpers
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
@@ -42,36 +62,43 @@
|
||||
(watch [_ _ _]
|
||||
(rx/of (dwsh/update-shapes [id] #(merge % attrs))))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; TOKENS Getters
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn get-tokens-lib
|
||||
[state]
|
||||
(-> (dsh/lookup-file-data state)
|
||||
(get :tokens-lib)))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; TOKENS Actions
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn create-token-theme [token-theme]
|
||||
(defn create-token-theme
|
||||
[token-theme]
|
||||
(let [new-token-theme token-theme]
|
||||
(ptk/reify ::create-token-theme
|
||||
ptk/WatchEvent
|
||||
(watch [it _ _]
|
||||
(let [changes (-> (pcb/empty-changes it)
|
||||
(pcb/add-token-theme new-token-theme))]
|
||||
(rx/of
|
||||
(dch/commit-changes changes)))))))
|
||||
(watch [it state _]
|
||||
(let [data (dsh/lookup-file-data state)
|
||||
tokens-lib (get data :tokens-lib)]
|
||||
|
||||
(if (and tokens-lib (ctob/get-theme tokens-lib (:group token-theme) (:name token-theme)))
|
||||
(rx/of (ntf/show {:content (tr "errors.token-theme-already-exists")
|
||||
:type :toast
|
||||
:level :error
|
||||
:timeout 9000}))
|
||||
(let [changes (-> (pcb/empty-changes it)
|
||||
(pcb/with-library-data data)
|
||||
(pcb/set-token-theme (:group new-token-theme)
|
||||
(:name new-token-theme)
|
||||
new-token-theme))]
|
||||
(rx/of (dch/commit-changes changes)))))))))
|
||||
|
||||
(defn update-token-theme [[group name] token-theme]
|
||||
(ptk/reify ::update-token-theme
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(let [tokens-lib (get-tokens-lib state)
|
||||
(let [tokens-lib (get-tokens-lib state)
|
||||
data (dsh/lookup-file-data state)
|
||||
prev-token-theme (some-> tokens-lib (ctob/get-theme group name))
|
||||
changes (pcb/update-token-theme (pcb/empty-changes it) token-theme prev-token-theme)]
|
||||
changes (-> (pcb/empty-changes it)
|
||||
(pcb/with-library-data data)
|
||||
(pcb/set-token-theme (:group prev-token-theme)
|
||||
(:name prev-token-theme)
|
||||
token-theme))]
|
||||
(rx/of
|
||||
(dch/commit-changes changes))))))
|
||||
|
||||
@@ -94,32 +121,42 @@
|
||||
(dch/commit-changes changes)
|
||||
(wtu/update-workspace-tokens))))))
|
||||
|
||||
(defn delete-token-theme [group name]
|
||||
(defn delete-token-theme [group theme-name]
|
||||
(ptk/reify ::delete-token-theme
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(let [data (dsh/lookup-file-data state)
|
||||
|
||||
changes (-> (pcb/empty-changes it)
|
||||
(pcb/with-library-data data)
|
||||
(pcb/delete-token-theme group name))]
|
||||
(pcb/set-token-theme group theme-name nil))]
|
||||
(rx/of
|
||||
(dch/commit-changes changes)
|
||||
(wtu/update-workspace-tokens))))))
|
||||
|
||||
(defn create-token-set [token-set]
|
||||
(let [new-token-set (merge
|
||||
{:name "Token Set"
|
||||
:tokens []}
|
||||
token-set)]
|
||||
(ptk/reify ::create-token-set
|
||||
ptk/WatchEvent
|
||||
(watch [it _ _]
|
||||
(let [changes (-> (pcb/empty-changes it)
|
||||
(pcb/add-token-set new-token-set))]
|
||||
(rx/of
|
||||
(dwts/set-selected-token-set-name (:name new-token-set))
|
||||
(dch/commit-changes changes)))))))
|
||||
(defn create-token-set
|
||||
[set-name]
|
||||
(ptk/reify ::create-token-set
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
;; Clear possible local state
|
||||
(update state :workspace-tokens dissoc :token-set-new-path))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(let [data (dsh/lookup-file-data state)
|
||||
tokens-lib (get data :tokens-lib)
|
||||
set-name (ctob/normalize-set-name set-name)]
|
||||
(if (and tokens-lib (ctob/get-set tokens-lib set-name))
|
||||
(rx/of (ntf/show {:content (tr "errors.token-set-already-exists")
|
||||
:type :toast
|
||||
:level :error
|
||||
:timeout 9000}))
|
||||
(let [token-set (ctob/make-token-set :name set-name)
|
||||
changes (-> (pcb/empty-changes it)
|
||||
(pcb/with-library-data data)
|
||||
(pcb/set-token-set set-name false token-set))]
|
||||
(rx/of (set-selected-token-set-name set-name)
|
||||
(dch/commit-changes changes))))))))
|
||||
|
||||
(defn rename-token-set-group [set-group-path set-group-fname]
|
||||
(ptk/reify ::rename-token-set-group
|
||||
@@ -130,32 +167,49 @@
|
||||
(rx/of
|
||||
(dch/commit-changes changes))))))
|
||||
|
||||
(defn update-token-set [set-name token-set]
|
||||
(defn update-token-set
|
||||
[token-set name]
|
||||
(ptk/reify ::update-token-set
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(let [prev-token-set (some-> (get-tokens-lib state)
|
||||
(ctob/get-set set-name))
|
||||
changes (-> (pcb/empty-changes it)
|
||||
(pcb/update-token-set token-set prev-token-set))]
|
||||
(rx/of
|
||||
(dwts/set-selected-token-set-name (:name token-set))
|
||||
(dch/commit-changes changes))))))
|
||||
(let [data (dsh/lookup-file-data state)
|
||||
name (ctob/normalize-set-name name (:name token-set))
|
||||
tokens-lib (get data :tokens-lib)]
|
||||
|
||||
(defn toggle-token-set [{:keys [token-set-name]}]
|
||||
(if (ctob/get-set tokens-lib name)
|
||||
(rx/of (ntf/show {:content (tr "errors.token-set-already-exists")
|
||||
:type :toast
|
||||
:level :error
|
||||
:timeout 9000}))
|
||||
(let [changes (-> (pcb/empty-changes it)
|
||||
(pcb/with-library-data data)
|
||||
(pcb/rename-token-set (:name token-set) name))]
|
||||
(rx/of (set-selected-token-set-name name)
|
||||
(dch/commit-changes changes))))))))
|
||||
|
||||
(defn toggle-token-set
|
||||
[name]
|
||||
(assert (string? name) "expected a string for `name`")
|
||||
(ptk/reify ::toggle-token-set
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [changes (clt/generate-toggle-token-set (pcb/empty-changes) (get-tokens-lib state) token-set-name)]
|
||||
(rx/of
|
||||
(dch/commit-changes changes)
|
||||
(wtu/update-workspace-tokens))))))
|
||||
(let [data (dsh/lookup-file-data state)
|
||||
tlib (get-tokens-lib state)
|
||||
changes (-> (pcb/empty-changes)
|
||||
(pcb/with-library-data data)
|
||||
(clt/generate-toggle-token-set tlib name))]
|
||||
|
||||
(rx/of (dch/commit-changes changes)
|
||||
(wtu/update-workspace-tokens))))))
|
||||
|
||||
(defn toggle-token-set-group [group-path]
|
||||
(ptk/reify ::toggle-token-set-group
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [changes (clt/generate-toggle-token-set-group (pcb/empty-changes) (get-tokens-lib state) group-path)]
|
||||
(let [data (dsh/lookup-file-data state)
|
||||
changes (-> (pcb/empty-changes)
|
||||
(pcb/with-library-data data)
|
||||
(clt/generate-toggle-token-set-group (get-tokens-lib state) group-path))]
|
||||
(rx/of
|
||||
(dch/commit-changes changes)
|
||||
(wtu/update-workspace-tokens))))))
|
||||
@@ -165,38 +219,31 @@
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(let [data (dsh/lookup-file-data state)
|
||||
update-token-set-change (some-> lib
|
||||
(ctob/get-sets)
|
||||
(first)
|
||||
(:name)
|
||||
(dwts/set-selected-token-set-name))
|
||||
changes (-> (pcb/empty-changes it)
|
||||
(pcb/with-library-data data)
|
||||
(pcb/set-tokens-lib lib))]
|
||||
(rx/of
|
||||
(dch/commit-changes changes)
|
||||
update-token-set-change
|
||||
(wtu/update-workspace-tokens))))))
|
||||
(rx/of (dch/commit-changes changes)
|
||||
(wtu/update-workspace-tokens))))))
|
||||
|
||||
(defn delete-token-set-path [group? path]
|
||||
(defn delete-token-set-path
|
||||
[group? path]
|
||||
(ptk/reify ::delete-token-set-path
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(let [data (dsh/lookup-file-data state)
|
||||
changes (-> (pcb/empty-changes it)
|
||||
(pcb/with-library-data data)
|
||||
(pcb/delete-token-set-path group? path))]
|
||||
(rx/of
|
||||
(dch/commit-changes changes)
|
||||
(wtu/update-workspace-tokens))))))
|
||||
(pcb/set-token-set (ctob/join-set-path path) group? nil))]
|
||||
(rx/of (dch/commit-changes changes)
|
||||
(wtu/update-workspace-tokens))))))
|
||||
|
||||
(defn drop-error [{:keys [error to-path]}]
|
||||
(ptk/reify ::drop-error
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(let [content (case error
|
||||
:path-exists (tr "errors.drag-drop.set-exists" to-path)
|
||||
:parent-to-child (tr "errors.drag-drop.parent-to-child")
|
||||
:path-exists (tr "errors.token-set-exists-on-drop" to-path)
|
||||
:parent-to-child (tr "errors.drop-token-set-parent-to-child")
|
||||
nil)]
|
||||
(when content
|
||||
(rx/of
|
||||
@@ -214,7 +261,7 @@
|
||||
(rx/of
|
||||
(dch/commit-changes changes)
|
||||
(wtu/update-workspace-tokens)))
|
||||
(catch js/Error e
|
||||
(catch :default e
|
||||
(rx/of
|
||||
(drop-error (ex-data e))))))))
|
||||
|
||||
@@ -224,48 +271,85 @@
|
||||
(watch [it state _]
|
||||
(try
|
||||
(when-let [changes (clt/generate-move-token-set (pcb/empty-changes it) (get-tokens-lib state) drop-opts)]
|
||||
(rx/of
|
||||
(dch/commit-changes changes)
|
||||
(some-> (get-in changes [:redo-changes 0 :to-path]) (dwts/set-selected-token-set-name))
|
||||
(wtu/update-workspace-tokens)))
|
||||
(catch js/Error e
|
||||
(rx/of (dch/commit-changes changes)
|
||||
(wtu/update-workspace-tokens)))
|
||||
(catch :default e
|
||||
(rx/of
|
||||
(drop-error (ex-data e))))))))
|
||||
|
||||
(defn update-create-token
|
||||
[{:keys [token prev-token-name]}]
|
||||
(ptk/reify ::update-create-token
|
||||
(defn- create-token-with-set
|
||||
"A special case when a first token is created and no set exists"
|
||||
[token]
|
||||
(ptk/reify ::create-token-and-set
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [token-set (dwts/get-selected-token-set state)
|
||||
token-set-name (or (:name token-set) "Global")
|
||||
changes (if (not token-set)
|
||||
;; No set created add a global set
|
||||
(let [tokens-lib (get-tokens-lib state)
|
||||
token-set (ctob/make-token-set :name token-set-name :tokens {(:name token) token})
|
||||
hidden-theme (ctob/make-hidden-token-theme :sets [token-set-name])
|
||||
active-theme-paths (some-> tokens-lib ctob/get-active-theme-paths)
|
||||
add-to-hidden-theme? (= active-theme-paths #{ctob/hidden-token-theme-path})
|
||||
base-changes (pcb/add-token-set (pcb/empty-changes) token-set)]
|
||||
(cond
|
||||
(not tokens-lib) (-> base-changes
|
||||
(pcb/add-token-theme hidden-theme)
|
||||
(pcb/update-active-token-themes #{ctob/hidden-token-theme-path} #{}))
|
||||
(let [data
|
||||
(dsh/lookup-file-data state)
|
||||
|
||||
add-to-hidden-theme? (let [prev-hidden-theme (ctob/get-theme tokens-lib ctob/hidden-token-theme-group ctob/hidden-token-theme-name)]
|
||||
(-> base-changes
|
||||
(pcb/update-token-theme (ctob/toggle-set prev-hidden-theme ctob/hidden-token-theme-path) prev-hidden-theme)))
|
||||
set-name
|
||||
"Global"
|
||||
|
||||
:else base-changes))
|
||||
;; Either update or add token to existing set
|
||||
(if-let [prev-token (ctob/get-token token-set (or prev-token-name (:name token)))]
|
||||
(pcb/update-token (pcb/empty-changes) (:name token-set) token prev-token)
|
||||
(do
|
||||
(st/emit! (ptk/event ::ev/event {::ev/name "create-tokens"}))
|
||||
(pcb/add-token (pcb/empty-changes) (:name token-set) token))))]
|
||||
(rx/of
|
||||
(dwts/set-selected-token-set-name token-set-name)
|
||||
(dch/commit-changes changes))))))
|
||||
token-set
|
||||
(-> (ctob/make-token-set :name set-name)
|
||||
(ctob/add-token token))
|
||||
|
||||
hidden-theme
|
||||
(ctob/make-hidden-token-theme)
|
||||
|
||||
hidden-theme-with-set
|
||||
(ctob/enable-set hidden-theme set-name)
|
||||
|
||||
changes
|
||||
(-> (pcb/empty-changes)
|
||||
(pcb/with-library-data data)
|
||||
(pcb/set-token-set set-name false token-set)
|
||||
(pcb/set-token-theme (:group hidden-theme)
|
||||
(:name hidden-theme)
|
||||
hidden-theme-with-set)
|
||||
(pcb/update-active-token-themes #{ctob/hidden-token-theme-path} #{}))]
|
||||
(rx/of (dch/commit-changes changes)
|
||||
(set-selected-token-set-name set-name))))))
|
||||
|
||||
(defn create-token
|
||||
[params]
|
||||
(let [token (ctob/make-token params)]
|
||||
(ptk/reify ::create-token
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(if-let [token-set (lookup-token-set state)]
|
||||
(let [data (dsh/lookup-file-data state)
|
||||
changes (-> (pcb/empty-changes it)
|
||||
(pcb/with-library-data data)
|
||||
(pcb/set-token (:name token-set)
|
||||
(:name token)
|
||||
token))]
|
||||
|
||||
(rx/of (dch/commit-changes changes)
|
||||
(ptk/data-event ::ev/event {::ev/name "create-token"})))
|
||||
|
||||
(rx/of (create-token-with-set token)))))))
|
||||
|
||||
(defn update-token
|
||||
[name params]
|
||||
(assert (string? name) "expected string for `name`")
|
||||
|
||||
(ptk/reify ::update-token
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(let [token-set (lookup-token-set state)
|
||||
data (dsh/lookup-file-data state)
|
||||
token (ctob/get-token token-set name)
|
||||
token' (->> (merge token params)
|
||||
(into {})
|
||||
(ctob/make-token))
|
||||
|
||||
changes (-> (pcb/empty-changes it)
|
||||
(pcb/with-library-data data)
|
||||
(pcb/set-token (:name token-set)
|
||||
(:name token)
|
||||
token'))]
|
||||
|
||||
(rx/of (dch/commit-changes changes))))))
|
||||
|
||||
(defn delete-token
|
||||
[set-name token-name]
|
||||
@@ -277,7 +361,7 @@
|
||||
(let [data (dsh/lookup-file-data state)
|
||||
changes (-> (pcb/empty-changes it)
|
||||
(pcb/with-library-data data)
|
||||
(pcb/delete-token set-name token-name))]
|
||||
(pcb/set-token set-name token-name nil))]
|
||||
(rx/of (dch/commit-changes changes))))))
|
||||
|
||||
(defn duplicate-token
|
||||
@@ -286,46 +370,99 @@
|
||||
(ptk/reify ::duplicate-token
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(when-let [token (some-> (dwts/get-selected-token-set-token state token-name)
|
||||
(update :name #(str/concat % "-copy")))]
|
||||
(rx/of
|
||||
(update-create-token {:token token}))))))
|
||||
(when-let [token-set (lookup-token-set state)]
|
||||
(when-let [token (ctob/get-token token-set token-name)]
|
||||
(let [tokens (ctob/get-tokens token-set)
|
||||
unames (map :name tokens)
|
||||
|
||||
suffix-fn
|
||||
(fn [copy-count]
|
||||
(let [suffix (tr "workspace.token.duplicate-suffix")]
|
||||
(str/concat "-"
|
||||
suffix
|
||||
(when (> copy-count 1)
|
||||
(str "-" copy-count)))))
|
||||
|
||||
copy-name
|
||||
(cfh/generate-unique-name token-name unames :suffix-fn suffix-fn)]
|
||||
|
||||
(rx/of (create-token (assoc token :name copy-name)))))))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; TOKEN UI OPS
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn set-token-type-section-open
|
||||
[token-type open?]
|
||||
(ptk/reify ::set-token-type-section-open
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc-in state [:workspace-tokens :open-status token-type] open?))))
|
||||
(update-in state [:workspace-tokens :open-status-by-type] assoc token-type open?))))
|
||||
|
||||
;; === Token Context Menu
|
||||
(defn assign-token-context-menu
|
||||
[{:keys [position] :as params}]
|
||||
|
||||
(when params
|
||||
(assert (gpt/point? position) "expected a point instance for `position` param"))
|
||||
|
||||
(defn show-token-context-menu
|
||||
[{:keys [position _token-name] :as params}]
|
||||
(dm/assert! (gpt/point? position))
|
||||
(ptk/reify ::show-token-context-menu
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc-in state [:workspace-local :token-context-menu] params))))
|
||||
(if params
|
||||
(update state :workspace-tokens assoc :token-context-menu params)
|
||||
(update state :workspace-tokens dissoc :token-context-menu)))))
|
||||
|
||||
(def hide-token-context-menu
|
||||
(ptk/reify ::hide-token-context-menu
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; TOKEN-SET UI OPS
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn assign-token-set-context-menu
|
||||
[{:keys [position] :as params}]
|
||||
(when params
|
||||
(assert (gpt/point? position) "expected valid point for `position` param"))
|
||||
|
||||
(ptk/reify ::assign-token-set-context-menu
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc-in state [:workspace-local :token-context-menu] nil))))
|
||||
(if params
|
||||
(update state :workspace-tokens assoc :token-set-context-menu params)
|
||||
(update state :workspace-tokens dissoc :token-set-context-menu)))))
|
||||
|
||||
;; === Token Set Context Menu
|
||||
|
||||
(defn show-token-set-context-menu
|
||||
[{:keys [position _token-set-name] :as params}]
|
||||
(dm/assert! (gpt/point? position))
|
||||
(ptk/reify ::show-token-set-context-menu
|
||||
(defn set-selected-token-set-name
|
||||
[name]
|
||||
(ptk/reify ::set-selected-token-set-name
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc-in state [:workspace-local :token-set-context-menu] params))))
|
||||
(update state :workspace-tokens assoc :selected-token-set-name name))))
|
||||
|
||||
(def hide-token-set-context-menu
|
||||
(ptk/reify ::hide-token-set-context-menu
|
||||
(defn start-token-set-edition
|
||||
[edition-id]
|
||||
(assert (string? edition-id) "expected a string for `edition-id`")
|
||||
|
||||
(ptk/reify ::start-token-set-edition
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc-in state [:workspace-local :token-set-context-menu] nil))))
|
||||
(update state :workspace-tokens assoc :token-set-edition-id edition-id))))
|
||||
|
||||
(defn start-token-set-creation
|
||||
[path]
|
||||
(assert (vector? path) "expected a vector for `path`")
|
||||
|
||||
(ptk/reify ::start-token-set-creation
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update state :workspace-tokens assoc :token-set-new-path path))))
|
||||
|
||||
(defn clear-token-set-edition
|
||||
[]
|
||||
(ptk/reify ::clear-token-set-edition
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update state :workspace-tokens dissoc :token-set-edition-id))))
|
||||
|
||||
(defn clear-token-set-creation
|
||||
[]
|
||||
(ptk/reify ::clear-token-set-creation
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update state :workspace-tokens dissoc :token-set-new-path))))
|
||||
|
||||
@@ -291,7 +291,8 @@
|
||||
(watch [_ state stream]
|
||||
(let [features (features/get-team-enabled-features state)
|
||||
render-wasm? (contains? features "render-wasm/v1")
|
||||
stopper-s (rx/filter (ptk/type? ::finalize-workspace) stream)]
|
||||
stopper-s (rx/filter (ptk/type? ::finalize-workspace) stream)
|
||||
team-id (:current-team-id state)]
|
||||
|
||||
(->> (rx/concat
|
||||
;; Firstly load wasm module if it is enabled and fonts
|
||||
@@ -305,7 +306,7 @@
|
||||
(rx/filter (ptk/type? ::df/fonts-loaded))
|
||||
(rx/take 1)
|
||||
(rx/ignore))
|
||||
(rx/of (df/fetch-fonts)))
|
||||
(rx/of (df/fetch-fonts team-id)))
|
||||
|
||||
;; Then fetch file and thumbnails
|
||||
(->> (rx/zip (rp/cmd! :get-file {:id file-id :features features})
|
||||
@@ -335,7 +336,7 @@
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(log/debug :hint "initialize-workspace" :file-id file-id)
|
||||
(log/debug :hint "initialize-workspace" :file-id (dm/str file-id))
|
||||
(let [stoper-s (rx/filter (ptk/type? ::finalize-workspace) stream)
|
||||
rparams (rt/get-params state)]
|
||||
|
||||
@@ -519,10 +520,8 @@
|
||||
(watch [it state _]
|
||||
(let [pages (-> (dsh/lookup-file-data state)
|
||||
(get :pages-index))
|
||||
|
||||
unames (cfh/get-used-names pages)
|
||||
name (cfh/generate-unique-name unames "Page 1")
|
||||
|
||||
name (cfh/generate-unique-name "Page" unames :immediate-suffix? true)
|
||||
changes (-> (pcb/empty-changes it)
|
||||
(pcb/add-empty-page id name))]
|
||||
|
||||
@@ -539,7 +538,13 @@
|
||||
page (get pages page-id)
|
||||
|
||||
unames (cfh/get-used-names pages)
|
||||
name (cfh/generate-unique-name unames (:name page))
|
||||
suffix-fn (fn [copy-count]
|
||||
(str/concat " "
|
||||
(tr "dashboard.copy-suffix")
|
||||
(when (> copy-count 1)
|
||||
(str " " copy-count))))
|
||||
base-name (:name page)
|
||||
name (cfh/generate-unique-name base-name unames :suffix-fn suffix-fn)
|
||||
objects (update-vals (:objects page) #(dissoc % :use-for-thumbnail))
|
||||
|
||||
main-instances-ids (set (keep #(when (ctk/main-instance? (val %)) (key %)) objects))
|
||||
@@ -554,7 +559,7 @@
|
||||
fdata
|
||||
(gpt/point (:x shape) (:y shape))
|
||||
true
|
||||
{:keep-ids? true})
|
||||
{:keep-ids? true :force-frame-id (:frame-id shape)})
|
||||
children (into {} (map (fn [shape] [(:id shape) shape]) new-shapes))
|
||||
objs (assoc objs id new-shape)]
|
||||
(merge objs children)))
|
||||
@@ -887,6 +892,7 @@
|
||||
(watch [it state _]
|
||||
(let [page-id (:current-page-id state)
|
||||
objects (dsh/lookup-page-objects state page-id)
|
||||
data (dsh/lookup-file-data state)
|
||||
|
||||
;; Ignore any shape whose parent is also intended to be moved
|
||||
ids (cfh/clean-loops objects ids)
|
||||
@@ -896,13 +902,15 @@
|
||||
|
||||
all-parents (into #{parent-id} (map #(cfh/get-parent-id objects %)) ids)
|
||||
|
||||
changes (cls/generate-relocate (pcb/empty-changes it)
|
||||
objects
|
||||
parent-id
|
||||
page-id
|
||||
to-index
|
||||
ids
|
||||
:ignore-parents? ignore-parents?)
|
||||
changes (-> (pcb/empty-changes it)
|
||||
(pcb/with-page-id page-id)
|
||||
(pcb/with-objects objects)
|
||||
(pcb/with-library-data data)
|
||||
(cls/generate-relocate
|
||||
parent-id
|
||||
to-index
|
||||
ids
|
||||
:ignore-parents? ignore-parents?))
|
||||
undo-id (js/Symbol)]
|
||||
|
||||
(rx/of (dwu/start-undo-transaction undo-id)
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
[app.common.files.helpers :as cph]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.svg.path.shapes-to-path :as stp]
|
||||
[app.common.types.component :as ctc]
|
||||
[app.common.types.container :as ctn]
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.types.shape.layout :as ctl]
|
||||
@@ -99,6 +100,7 @@
|
||||
shapes (->> ordered-indexes
|
||||
(map (d/getf objects))
|
||||
(remove cph/frame-shape?)
|
||||
(remove ctc/is-variant?)
|
||||
(remove #(ctn/has-any-copy-parent? objects %)))]
|
||||
|
||||
(when-not (empty? shapes)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user