mirror of
https://github.com/penpot/penpot.git
synced 2025-06-14 01:41:38 +02:00
📚 Update Tech Guide about abstraction levels
This commit is contained in:
parent
ca2891d441
commit
2ddcd0ce15
11 changed files with 380 additions and 220 deletions
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
title: 3.7. Data Guide
|
||||
title: 3.08. Data Guide
|
||||
desc: Learn about data structures, code organization, file operations, migrations, shape editing, and component syncing. See Penpot's technical guide. Try it free!
|
||||
---
|
||||
|
||||
|
@ -30,217 +30,6 @@ all of this is important in general.
|
|||
Clojure (for example ending it with ? for boolean values), because this may
|
||||
cause problems when exporting.
|
||||
|
||||
## Code organization in abstraction levels
|
||||
|
||||
Initially, Penpot data model implementation was organized in a different way.
|
||||
We are currently in a process of reorganization. The objective is to have data
|
||||
manipulation code structured in abstraction layers, with well-defined
|
||||
boundaries.
|
||||
|
||||
At this moment the namespace structure is already organized as described here,
|
||||
but there is much code that does not comply with these rules, and needs to be
|
||||
moved or refactored. We expect to be refactoring existing modules incrementally,
|
||||
each time we do an important functionality change.
|
||||
|
||||
### Abstract data types
|
||||
|
||||
```text
|
||||
▾ common/
|
||||
▾ src/app/common/
|
||||
▾ types/
|
||||
file.cljc
|
||||
page.cljc
|
||||
shape.cljc
|
||||
color.cljc
|
||||
component.cljc
|
||||
...
|
||||
```
|
||||
|
||||
Namespaces here represent a single data structure, or a fragment of one, as an
|
||||
abstract data type. Each structure has:
|
||||
|
||||
* A **schema spec** that defines the structure of the type and its values:
|
||||
|
||||
```clojure
|
||||
(sm/define! ::fill
|
||||
[:map {:title "Fill"}
|
||||
[:fill-color {:optional true} ::ctc/rgb-color]
|
||||
[:fill-opacity {:optional true} ::sm/safe-number]
|
||||
...)
|
||||
|
||||
(sm/define! ::shape-attrs
|
||||
[:map {:title "ShapeAttrs"}
|
||||
[:name {:optional true} :string]
|
||||
[:selrect {:optional true} ::grc/rect]
|
||||
[:points {:optional true} ::points]
|
||||
[:blocked {:optional true} :boolean]
|
||||
[:fills {:optional true}
|
||||
[:vector {:gen/max 2} ::fill]]
|
||||
...)
|
||||
```
|
||||
|
||||
* **Helper functions** to create, query and manipulate the structure. Helpers
|
||||
at this level only are allowed to see the internal attributes of a type.
|
||||
Updaters receive an object of the type, and return a new object modified,
|
||||
also ensuring the internal integrity of the data after the change.
|
||||
|
||||
```clojure
|
||||
(defn setup-shape
|
||||
"A function that initializes the geometric data of the shape. The props must
|
||||
contain at least :x :y :width :height."
|
||||
[{:keys [type] :as props}]
|
||||
...)
|
||||
|
||||
(defn has-direction?
|
||||
[interaction]
|
||||
(#{:slide :push} (-> interaction :animation :animation-type)))
|
||||
|
||||
(defn set-direction
|
||||
[interaction direction]
|
||||
(dm/assert!
|
||||
"expected valid interaction map"
|
||||
(check-interaction! interaction))
|
||||
(dm/assert!
|
||||
"expected valid direction"
|
||||
(contains? direction-types direction))
|
||||
(dm/assert!
|
||||
"expected compatible interaction map"
|
||||
(has-direction? interaction))
|
||||
(update interaction :animation assoc :direction direction))
|
||||
```
|
||||
|
||||
> IMPORTANT: we should always use helper functions to access and modify these data
|
||||
> structures. Avoid direct attribute read or using functions like <code class="language-clojure">assoc</code> or
|
||||
> <code class="language-clojure">update</code>, even if the information is contained in a single attribute. This way
|
||||
> it will be much simpler to add validation checks or to modify the internal
|
||||
> representation of a type, and will be easier to search for places in the code
|
||||
> where this data item is used.
|
||||
>
|
||||
> Currently much of Penpot code does not follow this requirement, but it
|
||||
should do in new code or in any refactor.
|
||||
|
||||
### File operations
|
||||
|
||||
```text
|
||||
▾ common/
|
||||
▾ src/app/common/
|
||||
▾ files/
|
||||
helpers.cljc
|
||||
shapes_helpers.cljc
|
||||
...
|
||||
```
|
||||
|
||||
Functions that modify a file object (or a part of it) in place, returning the
|
||||
file object changed. They ensure the referential integrity within the file, or
|
||||
between a file and its libraries.
|
||||
|
||||
```clojure
|
||||
(defn resolve-component
|
||||
"Retrieve the referenced component, from the local file or from a library"
|
||||
[shape file libraries & {:keys [include-deleted?] :or {include-deleted? False}}]
|
||||
(if (= (:component-file shape) (:id file))
|
||||
(ctkl/get-component (:data file) (:component-id shape) include-deleted?)
|
||||
(get-component libraries
|
||||
(:component-file shape)
|
||||
(:component-id shape)
|
||||
:include-deleted? include-deleted?)))
|
||||
|
||||
(defn delete-component
|
||||
"Mark a component as deleted and store the main instance shapes inside it, to
|
||||
be able to be recovered later."
|
||||
[file-data component-id skip-undelete? Main-instance]
|
||||
(let [components-v2 (dm/get-in file-data [:options :components-v2])]
|
||||
(if (or (not components-v2) skip-undelete?)
|
||||
(ctkl/delete-component file-data component-id)
|
||||
(let [set-main-instance ;; If there is a saved main-instance, restore it.
|
||||
#(if main-instance
|
||||
(assoc-in % [:objects (:main-instance-id %)] main-instance)
|
||||
%)]
|
||||
(-> file-data
|
||||
(ctkl/update-component component-id load-component-objects)
|
||||
(ctkl/update-component component-id set-main-instance)
|
||||
(ctkl/mark-component-deleted component-id))))))
|
||||
```
|
||||
|
||||
> This module is still needing an important refactor. Mainly to take functions
|
||||
> from common.types and move them here.
|
||||
|
||||
#### File validation and repair
|
||||
|
||||
There is a function in <code class="language-clojure">app.common.files.validate</code> that checks a file for
|
||||
referential and semantic integrity. It's called automatically when file changes
|
||||
are sent to backend, but may be invoked manually whenever it's needed.
|
||||
|
||||
### File changes objects
|
||||
|
||||
```text
|
||||
▾ common/
|
||||
▾ src/app/common/
|
||||
▾ files/
|
||||
changes_builder.cljc
|
||||
changes.cljc
|
||||
...
|
||||
```
|
||||
|
||||
Wrap the update functions in file operations module into <code class="language-clojure">changes</code> objects, that
|
||||
may be serialized, stored, sent to backend and executed to actually modify a file
|
||||
object. They should not contain business logic or algorithms. Only adapt the
|
||||
interface to the file operations or types.
|
||||
|
||||
```clojure
|
||||
(sm/define! ::changes
|
||||
[:map {:title "changes"}
|
||||
[:redo-changes vector?]
|
||||
[:undo-changes seq?]
|
||||
[:origin {:optional true} any?]
|
||||
[:save-undo? {:optional true} boolean?]
|
||||
[:stack-undo? {:optional true} boolean?]
|
||||
[:undo-group {:optional true} any?]])
|
||||
|
||||
(defmethod process-change :add-component
|
||||
[file-data params]
|
||||
(ctkl/add-component file-data params))
|
||||
```
|
||||
|
||||
### Business logic
|
||||
|
||||
```text
|
||||
▾ common/
|
||||
▾ src/app/common/
|
||||
▾ logic/
|
||||
shapes.cljc
|
||||
libraries.cljc
|
||||
```
|
||||
|
||||
Functions that implement semantic user actions, in an abstract way (independent
|
||||
of UI). They don't directly modify files, but generate changes objects, that
|
||||
may be executed in frontend or sent to backend.
|
||||
|
||||
```clojure
|
||||
(defn generate-instantiate-component
|
||||
"Generate changes to create a new instance from a component."
|
||||
[changes objects file-id component-id position page libraries old-id parent-id
|
||||
frame-id {:keys [force-frame?] :or {force-frame? False}}]
|
||||
(let [component (ctf/get-component libraries file-id component-id)
|
||||
parent (when parent-id (get objects parent-id))
|
||||
library (get libraries file-id)
|
||||
components-v2 (dm/get-in library [:data :options :components-v2])
|
||||
[new-shape new-shapes]º
|
||||
(ctn/make-component-instance page
|
||||
Component
|
||||
(:data library)
|
||||
Position
|
||||
Components-v2
|
||||
(cond-> {}
|
||||
force-frame? (assoc :force-frame-id frame-id)))
|
||||
changes (cond-> (pcb/add-object changes first-shape {:ignore-touched true})
|
||||
(some? old-id) (pcb/amend-last-change #(assoc % :old-id old-id)))
|
||||
changes (reduce #(pcb/add-object %1 %2 {:ignore-touched true})
|
||||
changes
|
||||
(rest new-shapes))]
|
||||
[new-shape changes]))
|
||||
```
|
||||
|
||||
## Data migrations
|
||||
|
||||
```text
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue