mirror of
https://github.com/penpot/penpot.git
synced 2026-03-22 18:33:45 +00:00
Merge remote-tracking branch 'origin/staging' into develop
This commit is contained in:
@@ -1,10 +0,0 @@
|
||||
name: New MCP server
|
||||
version: 0.0.1
|
||||
schema: v1
|
||||
mcpServers:
|
||||
- name: New MCP server
|
||||
command: npx
|
||||
args:
|
||||
- -y
|
||||
- <your-mcp-server>
|
||||
env: {}
|
||||
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
@@ -272,7 +272,7 @@ jobs:
|
||||
- name: Build Bundle
|
||||
working-directory: ./frontend
|
||||
run: |
|
||||
./scripts/build 0.0.0
|
||||
./scripts/build
|
||||
|
||||
- name: Store Bundle Cache
|
||||
uses: actions/cache@v5
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -57,11 +57,13 @@
|
||||
/frontend/package-lock.json
|
||||
/frontend/resources/fonts/experiments
|
||||
/frontend/resources/public/*
|
||||
/frontend/src/app/render_wasm/api/shared.js
|
||||
/frontend/storybook-static/
|
||||
/frontend/target/
|
||||
/frontend/test-results/
|
||||
/frontend/.shadow-cljs
|
||||
/other/
|
||||
/scripts/
|
||||
/nexus/
|
||||
/tmp/
|
||||
/vendor/**/target
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
export PENPOT_NITRATE_SHARED_KEY=super-secret-nitrate-api-key
|
||||
export PENPOT_EXPORTER_SHARED_KEY=super-secret-exporter-api-key
|
||||
export PENPOT_NEXUS_SHARED_KEY=super-secret-nexus-api-key
|
||||
export PENPOT_SECRET_KEY=super-secret-devenv-key
|
||||
|
||||
# DEPRECATED: only used for subscriptions
|
||||
|
||||
@@ -103,6 +103,7 @@
|
||||
|
||||
[:exporter-shared-key {:optional true} :string]
|
||||
[:nitrate-shared-key {:optional true} :string]
|
||||
[:nexus-shared-key {:optional true} :string]
|
||||
[:management-api-key {:optional true} :string]
|
||||
|
||||
[:telemetry-uri {:optional true} :string]
|
||||
|
||||
@@ -120,7 +120,7 @@
|
||||
;; an external storage and data cleared.
|
||||
|
||||
(def ^:private schema:event
|
||||
[:map {:title "event"}
|
||||
[:map {:title "AuditEvent"}
|
||||
[::type ::sm/text]
|
||||
[::name ::sm/text]
|
||||
[::profile-id ::sm/uuid]
|
||||
|
||||
@@ -10,14 +10,11 @@
|
||||
[app.common.logging :as l]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.transit :as t]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.http.client :as http]
|
||||
[app.setup :as-alias setup]
|
||||
[app.tokens :as tokens]
|
||||
[integrant.core :as ig]
|
||||
[lambdaisland.uri :as u]
|
||||
[promesa.exec :as px]))
|
||||
|
||||
;; This is a task responsible to send the accumulated events to
|
||||
@@ -52,19 +49,18 @@
|
||||
|
||||
(defn- send!
|
||||
[{:keys [::uri] :as cfg} events]
|
||||
(let [token (tokens/generate cfg
|
||||
{:iss "authentication"
|
||||
:uid uuid/zero})
|
||||
(let [skey (-> cfg ::setup/shared-keys :nexus)
|
||||
body (t/encode {:events events})
|
||||
headers {"content-type" "application/transit+json"
|
||||
"origin" (str (cf/get :public-uri))
|
||||
"cookie" (u/map->query-string {:auth-token token})}
|
||||
"x-shared-key" (str "nexus " skey)}
|
||||
params {:uri uri
|
||||
:timeout 12000
|
||||
:method :post
|
||||
:headers headers
|
||||
:body body}
|
||||
resp (http/req! cfg params)]
|
||||
|
||||
(if (= (:status resp) 204)
|
||||
true
|
||||
(do
|
||||
@@ -109,7 +105,7 @@
|
||||
(def ^:private schema:handler-params
|
||||
[:map
|
||||
::db/pool
|
||||
::setup/props
|
||||
::setup/shared-keys
|
||||
::http/client])
|
||||
|
||||
(defmethod ig/assert-key ::handler
|
||||
|
||||
@@ -466,16 +466,17 @@
|
||||
|
||||
::setup/shared-keys
|
||||
{::setup/props (ig/ref ::setup/props)
|
||||
:nitrate (cf/get :nitrate-shared-key)
|
||||
:exporter (cf/get :exporter-shared-key)}
|
||||
:nexus (cf/get :nexus-shared-key)
|
||||
:nitrate (cf/get :nitrate-shared-key)
|
||||
:exporter (cf/get :exporter-shared-key)}
|
||||
|
||||
::setup/clock
|
||||
{}
|
||||
|
||||
:app.loggers.audit.archive-task/handler
|
||||
{::setup/props (ig/ref ::setup/props)
|
||||
::db/pool (ig/ref ::db/pool)
|
||||
::http.client/client (ig/ref ::http.client/client)}
|
||||
{::setup/shared-keys (ig/ref ::setup/shared-keys)
|
||||
::http.client/client (ig/ref ::http.client/client)
|
||||
::db/pool (ig/ref ::db/pool)}
|
||||
|
||||
:app.loggers.audit.gc-task/handler
|
||||
{::db/pool (ig/ref ::db/pool)}
|
||||
|
||||
@@ -82,45 +82,37 @@
|
||||
(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")))
|
||||
(l/wrn :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)))))))
|
||||
|
||||
(sm/register! ::props [:map-of :keyword ::sm/any])
|
||||
|
||||
|
||||
(defmethod ig/init-key ::shared-keys
|
||||
[_ {:keys [::props] :as cfg}]
|
||||
(let [secret (get props :secret-key)]
|
||||
(d/without-nils
|
||||
{:exporter
|
||||
(let [key (or (get cfg :exporter)
|
||||
(-> (keys/derive secret :salt "exporter")
|
||||
(bc/bytes->b64-str true)))]
|
||||
(if (or (str/empty? key)
|
||||
(str/blank? key))
|
||||
(do
|
||||
(l/wrn :hint "exporter key is disabled because empty string found")
|
||||
nil)
|
||||
(do
|
||||
(l/inf :hint "exporter key initialized" :key (d/obfuscate-string key))
|
||||
key)))
|
||||
(reduce (fn [keys id]
|
||||
(let [key (or (get cfg id)
|
||||
(-> (keys/derive secret :salt (name id))
|
||||
(bc/bytes->b64-str true)))]
|
||||
(if (or (str/empty? key)
|
||||
(str/blank? key))
|
||||
(do
|
||||
(l/wrn :id (name id) :hint "key is disabled because empty string found")
|
||||
keys)
|
||||
(do
|
||||
(l/inf :id (name id) :hint "key initialized" :key (d/obfuscate-string key))
|
||||
(assoc keys id key)))))
|
||||
{}
|
||||
[:exporter
|
||||
:nitrate
|
||||
:nexus])))
|
||||
|
||||
:nitrate
|
||||
(let [key (or (get cfg :nitrate)
|
||||
(-> (keys/derive secret :salt "nitrate")
|
||||
(bc/bytes->b64-str true)))]
|
||||
(if (or (str/empty? key)
|
||||
(str/blank? key))
|
||||
(do
|
||||
(l/wrn :hint "nitrate key is disabled because empty string found")
|
||||
nil)
|
||||
(do
|
||||
(l/inf :hint "nitrate key initialized" :key (d/obfuscate-string key))
|
||||
key)))})))
|
||||
(sm/register! ::props [:map-of :keyword ::sm/any])
|
||||
(sm/register! ::shared-keys [:map-of :keyword ::sm/text])
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.common.schema
|
||||
(:refer-clojure :exclude [deref merge parse-uuid parse-long parse-double parse-boolean type keys])
|
||||
(:refer-clojure :exclude [deref merge parse-uuid parse-long parse-double parse-boolean type keys select-keys])
|
||||
#?(:cljs (:require-macros [app.common.schema :refer [ignoring]]))
|
||||
(:require
|
||||
#?(:clj [malli.dev.pretty :as mdp])
|
||||
@@ -93,6 +93,11 @@
|
||||
[& items]
|
||||
(apply mu/merge (map schema items)))
|
||||
|
||||
(defn select-keys
|
||||
[s keys & {:as opts}]
|
||||
(let [s (schema s)]
|
||||
(mu/select-keys s keys opts)))
|
||||
|
||||
(defn assoc-key
|
||||
"Add a key & value to a schema of type [:map]. If the first level node of the schema
|
||||
is not a map, will do a depth search to find the first map node and add the key there."
|
||||
@@ -138,10 +143,10 @@
|
||||
(mu/optional-keys schema keys default-options)))
|
||||
|
||||
(defn required-keys
|
||||
([schema]
|
||||
(mu/required-keys schema nil default-options))
|
||||
([schema keys]
|
||||
(mu/required-keys schema keys default-options)))
|
||||
([s]
|
||||
(mu/required-keys (schema s) nil default-options))
|
||||
([s keys]
|
||||
(mu/required-keys (schema s) keys default-options)))
|
||||
|
||||
(defn transformer
|
||||
[& transformers]
|
||||
@@ -646,7 +651,7 @@
|
||||
{:title "set"
|
||||
:description "Set of Strings"
|
||||
:error/message "should be a set of strings"
|
||||
:gen/gen (-> kind sg/generator sg/set)
|
||||
:gen/gen (sg/mcat (fn [_] (sg/generator kind)) sg/int)
|
||||
:decode/string decode
|
||||
:decode/json decode
|
||||
:encode/string encode-string
|
||||
|
||||
@@ -190,10 +190,14 @@
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn get-points
|
||||
"Returns points for the given segment, faster version of
|
||||
the `content->points`."
|
||||
"Returns points for the given content. Accepts PathData instances or
|
||||
plain segment vectors. Returns nil for nil content."
|
||||
[content]
|
||||
(some-> content segment/get-points))
|
||||
(when (some? content)
|
||||
(let [content (if (impl/path-data? content)
|
||||
content
|
||||
(impl/path-data content))]
|
||||
(segment/get-points content))))
|
||||
|
||||
(defn calc-selrect
|
||||
"Calculate selrect from a content. The content can be in a PathData
|
||||
|
||||
@@ -52,14 +52,15 @@
|
||||
[target key & expr]
|
||||
(if (:ns &env)
|
||||
(let [target (with-meta target {:tag 'js})]
|
||||
`(let [~'cache (.-cache ~target)
|
||||
~'result (.get ~'cache ~key)]
|
||||
(if ~'result
|
||||
(do
|
||||
~'result)
|
||||
(let [~'result (do ~@expr)]
|
||||
(.set ~'cache ~key ~'result)
|
||||
~'result))))
|
||||
`(let [~'cache (.-cache ~target)]
|
||||
(if (some? ~'cache)
|
||||
(let [~'result (.get ~'cache ~key)]
|
||||
(if ~'result
|
||||
~'result
|
||||
(let [~'result (do ~@expr)]
|
||||
(.set ~'cache ~key ~'result)
|
||||
~'result)))
|
||||
(do ~@expr))))
|
||||
`(do ~@expr)))
|
||||
|
||||
(defn- impl-transform-segment
|
||||
|
||||
@@ -279,6 +279,12 @@
|
||||
(t/is (some? points))
|
||||
(t/is (= 3 (count points))))))
|
||||
|
||||
(t/deftest path-get-points-plain-vector-safe
|
||||
(t/testing "path/get-points does not throw for plain vector content"
|
||||
(let [points (path/get-points sample-content)]
|
||||
(t/is (some? points))
|
||||
(t/is (= 3 (count points))))))
|
||||
|
||||
(defn calculate-extremities
|
||||
"Calculate extremities for the provided content.
|
||||
A legacy implementation used mainly as reference for testing"
|
||||
|
||||
@@ -70,9 +70,11 @@
|
||||
</main>
|
||||
|
||||
<div class="pre-footer">
|
||||
<a href="https://github.com/penpot/penpot/blob/main/docs/{{ page.inputPath }}">Edit this page on GitHub</a>
|
||||
or ask a
|
||||
<a href="https://penpot.app/talk-to-us" target="_blank">question</a>.
|
||||
<div>Found an issue or want to improve this page?<br><br>
|
||||
<a href="https://github.com/penpot/penpot/blob/main/docs/{{ page.inputPath }}">Edit this page on GitHub</a>
|
||||
·
|
||||
<a href="https://github.com/penpot/penpot/issues/new/choose" target="_blank">Open an issue</a>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="footer">
|
||||
<div class="footer-inside">
|
||||
|
||||
@@ -57,5 +57,5 @@ eleventyNavigation:
|
||||
|
||||
<div class="contact-block">
|
||||
<h2>Contact us</h2>
|
||||
<p>Need help? <a href="https://penpot.app/talk-to-us" target="_blank">Talk to us</a> or join our <a href="https://community.penpot.app/" target="_blank">community</a>.</p>
|
||||
<p>Write us at <a href="mailto:support@penpot.app" target="_blank">support@penpot.app</a> or join our <a href="https://community.penpot.app/" target="_blank">Community</a>.</p>
|
||||
</div>
|
||||
|
||||
@@ -187,7 +187,54 @@ python3 manage.py create-profile --skip-tutorial --skip-walkthrough
|
||||
python3 manage.py create-profile -n "Jane Doe" -e jane@example.com -p secretpassword --skip-tutorial --skip-walkthrough
|
||||
```
|
||||
|
||||
## Team Feature Flags
|
||||
## Feature Flags
|
||||
|
||||
### Frontend flags via config.js
|
||||
|
||||
You can enable or disable feature flags on the frontend by creating (or editing) a
|
||||
`config.js` file at `frontend/resources/public/js/config.js`. This file is
|
||||
**gitignored**, so it has to be created manually. Your local flags won't affect other developers.
|
||||
|
||||
Set the `penpotFlags` variable with a space-separated list of flags:
|
||||
|
||||
```js
|
||||
var penpotFlags = "enable-mcp enable-webhooks enable-access-tokens";
|
||||
```
|
||||
|
||||
Each flag entry uses the format `enable-<flag>` or `disable-<flag>`. They are
|
||||
merged on top of the built-in defaults, so you only need to list the flags you want
|
||||
to change.
|
||||
|
||||
Some examples of commonly used flags:
|
||||
|
||||
- `enable-access-tokens` — enables the Access Tokens section under profile settings.
|
||||
- `enable-mcp` — enables the MCP server configuration section.
|
||||
- `enable-webhooks` — enables webhooks configuration.
|
||||
- `enable-login-with-ldap` — enables LDAP login.
|
||||
|
||||
The full list of available flags can be found in `common/src/app/common/flags.cljc`.
|
||||
|
||||
After creating or modifying this file, **reload the browser** (no need to restart anything).
|
||||
|
||||
### Backend flags via PENPOT_FLAGS
|
||||
|
||||
Backend feature flags are controlled through the `PENPOT_FLAGS` environment
|
||||
variable using the same `enable-<flag>` / `disable-<flag>` format. You can set
|
||||
this in the `docker/devenv/docker-compose.yaml` file under the `main` service
|
||||
`environment` section:
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
- PENPOT_FLAGS=enable-access-tokens enable-mcp
|
||||
```
|
||||
|
||||
This requires **restarting the backend** to take effect.
|
||||
|
||||
> **Note**: Some features (e.g., access tokens, webhooks) need both frontend and
|
||||
> backend flags enabled to work end-to-end. The frontend flag enables the UI, while
|
||||
> the backend flag enables the corresponding API endpoints.
|
||||
|
||||
### Team Feature Flags
|
||||
|
||||
To test a Feature Flag, you can enable or disable them by team through the `dbg` page:
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"lint:js": "exit 0",
|
||||
"lint:scss": "exit 0",
|
||||
"build:test": "clojure -M:dev:shadow-cljs compile test",
|
||||
"test": "pnpm run build:test && node target/tests/test.js",
|
||||
"test": "pnpm run build:wasm && pnpm run build:test && node target/tests/test.js",
|
||||
"test:storybook": "vitest run --project=storybook",
|
||||
"watch:test": "mkdir -p target/tests && concurrently \"clojure -M:dev:shadow-cljs watch test\" \"nodemon -C -d 2 -w target/tests --exec 'node target/tests/test.js'\"",
|
||||
"test:e2e": "playwright test --project default",
|
||||
|
||||
@@ -70,7 +70,8 @@
|
||||
|
||||
:release
|
||||
{:closure-defines {goog.DEBUG false
|
||||
goog.debug.LOGGING_ENABLED true}
|
||||
goog.debug.LOGGING_ENABLED true
|
||||
app.config/compiled-version-tag #shadow/env ["VERSION_TAG" :default "develop"]}
|
||||
:compiler-options
|
||||
{:fn-invoke-direct true
|
||||
:optimizations #shadow/env ["PENPOT_BUILD_OPTIMIZATIONS" :as :keyword :default :advanced]
|
||||
|
||||
@@ -6,8 +6,11 @@
|
||||
|
||||
(ns app.config
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.flags :as flags]
|
||||
[app.common.logging :as log]
|
||||
[app.common.time :as ct]
|
||||
[app.common.uri :as u]
|
||||
[app.common.version :as v]
|
||||
[app.util.avatars :as avatars]
|
||||
@@ -15,6 +18,8 @@
|
||||
[app.util.globals :refer [global location]]
|
||||
[app.util.navigator :as nav]
|
||||
[app.util.object :as obj]
|
||||
[app.util.storage :as sto]
|
||||
[app.util.timers :as ts]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
(set! *assert* js/goog.DEBUG)
|
||||
@@ -43,7 +48,7 @@
|
||||
^boolean (check-safari-16?) :safari-16
|
||||
^boolean (check-safari-17?) :safari-17
|
||||
^boolean (check-safari?) :safari
|
||||
:else :other)))
|
||||
:else :unknown)))
|
||||
|
||||
(defn- parse-platform
|
||||
[]
|
||||
@@ -81,6 +86,16 @@
|
||||
"unknown"
|
||||
date)))
|
||||
|
||||
;; --- Compile-time version tag
|
||||
;;
|
||||
;; This value is baked into the compiled JS at build time via closure-defines,
|
||||
;; so it travels with the JS bundle. In contrast, `version-tag` (below) is read
|
||||
;; at runtime from globalThis.penpotVersionTag which is set by the always-fresh
|
||||
;; index.html. Comparing the two lets us detect when the browser has loaded
|
||||
;; stale cached JS files.
|
||||
|
||||
(goog-define compiled-version-tag "develop")
|
||||
|
||||
;; --- Global Config Vars
|
||||
|
||||
(def default-theme "default")
|
||||
@@ -90,12 +105,50 @@
|
||||
|
||||
(def build-date (parse-build-date global))
|
||||
(def flags (parse-flags global))
|
||||
(def version (parse-version global))
|
||||
(def target (parse-target global))
|
||||
(def browser (parse-browser))
|
||||
(def platform (parse-platform))
|
||||
|
||||
(def version (parse-version global))
|
||||
(def version-tag (obj/get global "penpotVersionTag"))
|
||||
|
||||
(defn stale-build?
|
||||
"Returns true when the compiled JS was built with a different version
|
||||
tag than the one present in the current index.html. This indicates
|
||||
the browser has cached JS from a previous deployment."
|
||||
^boolean
|
||||
[]
|
||||
(not= compiled-version-tag version-tag))
|
||||
|
||||
;; --- Throttled reload
|
||||
;;
|
||||
;; A generic reload mechanism with loop protection via sessionStorage.
|
||||
;; Used by both the boot-time stale-build check and the runtime
|
||||
;; stale-asset error handler.
|
||||
|
||||
(def ^:private reload-storage-key "penpot-last-reload-timestamp")
|
||||
(def ^:private reload-cooldown-ms 30000)
|
||||
|
||||
(defn throttled-reload
|
||||
"Force a hard page reload unless one was already triggered within the
|
||||
last 30 seconds (tracked in sessionStorage). Returns true when a
|
||||
reload is initiated, false when suppressed."
|
||||
[& {:keys [reason]}]
|
||||
(let [now (ct/now)
|
||||
prev-ts (-> (sto/get-item sto/session-storage reload-storage-key)
|
||||
(d/parse-integer))]
|
||||
(if (and (some? prev-ts)
|
||||
(< (- now prev-ts) reload-cooldown-ms))
|
||||
(do
|
||||
(log/wrn :hint "reload suppressed (cooldown active)"
|
||||
:reason reason)
|
||||
false)
|
||||
(do
|
||||
(log/wrn :hint "forcing page reload" :reason reason)
|
||||
(sto/set-item sto/session-storage reload-storage-key (str now))
|
||||
(ts/asap #(.reload ^js location true))
|
||||
true))))
|
||||
|
||||
(def terms-of-service-uri (obj/get global "penpotTermsOfServiceURI"))
|
||||
(def privacy-policy-uri (obj/get global "penpotPrivacyPolicyURI"))
|
||||
(def flex-help-uri (obj/get global "penpotGridHelpURI" "https://help.penpot.app/user-guide/flexible-layouts/"))
|
||||
|
||||
@@ -100,16 +100,24 @@
|
||||
|
||||
(defn ^:export init
|
||||
[options]
|
||||
(some-> (unchecked-get options "defaultTranslations")
|
||||
(i18n/set-default-translations))
|
||||
;; Before initializing anything, check if the browser has loaded
|
||||
;; stale JS from a previous deployment. If so, do a hard reload so
|
||||
;; the browser fetches fresh assets matching the current index.html.
|
||||
(if (cf/stale-build?)
|
||||
(cf/throttled-reload
|
||||
:reason (dm/str "stale JS: compiled=" cf/compiled-version-tag
|
||||
" expected=" cf/version-tag))
|
||||
(do
|
||||
(some-> (unchecked-get options "defaultTranslations")
|
||||
(i18n/set-default-translations))
|
||||
|
||||
(mw/init!)
|
||||
(i18n/init)
|
||||
(cur/init-styles)
|
||||
(mw/init!)
|
||||
(i18n/init)
|
||||
(cur/init-styles)
|
||||
|
||||
(init-ui)
|
||||
(st/emit! (plugins/initialize)
|
||||
(initialize)))
|
||||
(init-ui)
|
||||
(st/emit! (plugins/initialize)
|
||||
(initialize)))))
|
||||
|
||||
(defn ^:export reinit
|
||||
([]
|
||||
|
||||
@@ -362,7 +362,7 @@
|
||||
(ptk/reify ::toggle-project-pin
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc-in state [:projects id :is-pinned] (not is-pinned)))
|
||||
(d/update-in-when state [:projects id] assoc :is-pinned (not is-pinned)))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
@@ -379,7 +379,7 @@
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
(update-in [:projects id :name] (constantly name))
|
||||
(d/update-in-when [:projects id] assoc :name name)
|
||||
(update :dashboard-local dissoc :project-for-edit)))
|
||||
|
||||
ptk/WatchEvent
|
||||
@@ -409,7 +409,7 @@
|
||||
(ptk/reify ::file-deleted
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update-in state [:projects project-id :count] dec))))
|
||||
(d/update-in-when state [:projects project-id :count] dec))))
|
||||
|
||||
(defn delete-file
|
||||
[{:keys [id project-id] :as params}]
|
||||
@@ -514,7 +514,7 @@
|
||||
(-> state
|
||||
(assoc-in [:files id] file)
|
||||
(assoc-in [:recent-files id] file)
|
||||
(update-in [:projects project-id :count] inc))))))
|
||||
(d/update-in-when [:projects project-id :count] inc))))))
|
||||
|
||||
(defn create-file
|
||||
[{:keys [project-id name] :as params}]
|
||||
|
||||
@@ -373,7 +373,15 @@
|
||||
|
||||
(when (contains? cf/flags :mcp)
|
||||
(->> mbc/stream
|
||||
(rx/filter (mbc/type? :mcp-enabled-change))
|
||||
(rx/filter (mbc/type? :mcp-enabled-change-connection))
|
||||
(rx/map deref)
|
||||
(rx/mapcat (fn [value]
|
||||
(rx/of (mcp/update-mcp-connection value)
|
||||
(mcp/disconnect-mcp))))))
|
||||
|
||||
(when (contains? cf/flags :mcp)
|
||||
(->> mbc/stream
|
||||
(rx/filter (mbc/type? :mcp-enabled-change-status))
|
||||
(rx/map deref)
|
||||
(rx/map mcp/update-mcp-status)))
|
||||
|
||||
|
||||
@@ -9,10 +9,14 @@
|
||||
[app.common.logging :as log]
|
||||
[app.common.uri :as u]
|
||||
[app.config :as cf]
|
||||
[app.main.broadcast :as mbc]
|
||||
[app.main.data.event :as ev]
|
||||
[app.main.data.notifications :as ntf]
|
||||
[app.main.data.plugins :as dp]
|
||||
[app.main.repo :as rp]
|
||||
[app.main.store :as st]
|
||||
[app.plugins.register :refer [mcp-plugin-id]]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[beicon.v2.core :as rx]
|
||||
[potok.v2.core :as ptk]))
|
||||
|
||||
@@ -34,6 +38,37 @@
|
||||
[event]
|
||||
(= (ptk/type event) :app.main.data.workspace/finalize-workspace))
|
||||
|
||||
(defn disconnect-mcp
|
||||
[]
|
||||
(st/emit! (ptk/data-event ::disconnect)))
|
||||
|
||||
(defn connect-mcp
|
||||
[]
|
||||
(ptk/reify ::connect-mcp
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ stream]
|
||||
(mbc/emit! :mcp-enabled-change-connection false)
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? ::disconnect))
|
||||
(rx/take 1)
|
||||
(rx/map #(ptk/data-event ::connect))))))
|
||||
|
||||
(defn manage-notification
|
||||
[mcp-enabled? mcp-connected?]
|
||||
(if mcp-enabled?
|
||||
(if mcp-connected?
|
||||
(rx/of (ntf/hide))
|
||||
(rx/of (ntf/dialog :content (tr "notifications.mcp.active-tab-switching.text")
|
||||
:cancel {:label (tr "labels.dismiss")
|
||||
:callback #(st/emit! (ntf/hide)
|
||||
(ptk/event ::ev/event {::ev/name "confirm-mcp-tab-switch"
|
||||
::ev/origin "workspace-notification"}))}
|
||||
:accept {:label (tr "labels.switch")
|
||||
:callback #(st/emit! (connect-mcp)
|
||||
(ptk/event ::ev/event {::ev/name "dismiss-mcp-tab-switch"
|
||||
::ev/origin "workspace-notification"}))})))
|
||||
(rx/of (ntf/hide))))
|
||||
|
||||
(defn update-mcp-status
|
||||
[value]
|
||||
(ptk/reify ::update-mcp-status
|
||||
@@ -42,18 +77,26 @@
|
||||
(update-in state [:profile :props] assoc :mcp-enabled value))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(case value
|
||||
true (rx/of (ptk/data-event ::connect))
|
||||
false (rx/of (ptk/data-event ::disconnect))
|
||||
nil))))
|
||||
(watch [_ state _]
|
||||
(rx/merge
|
||||
(let [mcp-connected? (-> state :workspace-local :mcp :connected)]
|
||||
(manage-notification value mcp-connected?))
|
||||
(case value
|
||||
true (rx/of (ptk/data-event ::connect))
|
||||
false (rx/of (ptk/data-event ::disconnect))
|
||||
nil)))))
|
||||
|
||||
(defn update-mcp-connection
|
||||
[value]
|
||||
(ptk/reify ::update-mcp-plugin-connection
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update-in state [:workspace-local :mcp] assoc :connected value))))
|
||||
(update-in state [:workspace-local :mcp] assoc :connected value))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [mcp-enabled? (-> state :profile :props :mcp-enabled)]
|
||||
(manage-notification mcp-enabled? value)))))
|
||||
|
||||
(defn init-mcp!
|
||||
[stream]
|
||||
@@ -94,14 +137,6 @@
|
||||
(rx/take-until stopper)
|
||||
(rx/subs! #(cb))))))}}))))))
|
||||
|
||||
(defn disconnect-mcp
|
||||
[]
|
||||
(st/emit! (ptk/data-event ::disconnect)))
|
||||
|
||||
(defn connect-mcp
|
||||
[]
|
||||
(st/emit! (ptk/data-event ::connect)))
|
||||
|
||||
(defn init-mcp-connection
|
||||
[]
|
||||
(ptk/reify ::init-mcp-connection
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
(let [content (st/get-path state :content)
|
||||
content (if (and (not preserve-move-to)
|
||||
(= (-> content last :command) :move-to))
|
||||
(into [] (take (dec (count content)) content))
|
||||
(path/content (take (dec (count content)) content))
|
||||
content)]
|
||||
(st/set-content state content)))
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.types.path.segment :as path.segm]
|
||||
[app.common.types.path :as path]
|
||||
[app.main.data.workspace.path.state :as pst]
|
||||
[app.main.snap :as snap]
|
||||
[app.main.store :as st]
|
||||
@@ -167,7 +167,7 @@
|
||||
ranges-stream
|
||||
(->> content-stream
|
||||
(rx/filter some?)
|
||||
(rx/map path.segm/get-points)
|
||||
(rx/map path/get-points)
|
||||
(rx/map snap/create-ranges))]
|
||||
|
||||
(->> ms/mouse-position
|
||||
|
||||
@@ -33,6 +33,27 @@
|
||||
;; Will contain last uncaught exception
|
||||
(def last-exception nil)
|
||||
|
||||
;; --- Stale-asset error detection and auto-reload
|
||||
;;
|
||||
;; When the browser loads JS modules from different builds (e.g. shared.js from
|
||||
;; build A and main-dashboard.js from build B because you loaded it in the
|
||||
;; middle of a deploy per example), keyword constants referenced across modules
|
||||
;; will be undefined. This manifests as TypeError messages containing
|
||||
;; "$cljs$cst$" and "is undefined" or "is null".
|
||||
|
||||
(defn stale-asset-error?
|
||||
"Returns true if the error matches the signature of a cross-build
|
||||
module mismatch: accessing a ClojureScript keyword constant that
|
||||
doesn't exist on the shared $APP object."
|
||||
[cause]
|
||||
(when (some? cause)
|
||||
(let [message (ex-message cause)]
|
||||
(and (string? message)
|
||||
(str/includes? message "$cljs$cst$")
|
||||
(or (str/includes? message "is undefined")
|
||||
(str/includes? message "is null")
|
||||
(str/includes? message "is not a function"))))))
|
||||
|
||||
(defn exception->error-data
|
||||
[cause]
|
||||
(let [data (ex-data cause)]
|
||||
@@ -127,24 +148,15 @@
|
||||
(ex/print-throwable cause :prefix "Unexpected Error")
|
||||
(flash :cause cause :type :unhandled))))
|
||||
|
||||
(defmethod ptk/handle-error :wasm-non-blocking
|
||||
(defmethod ptk/handle-error :wasm-error
|
||||
[error]
|
||||
(when-let [cause (::instance error)]
|
||||
(flash :cause cause)))
|
||||
|
||||
(defmethod ptk/handle-error :wasm-critical
|
||||
[error]
|
||||
(when-let [cause (::instance error)]
|
||||
(ex/print-throwable cause :prefix "WASM critical error"))
|
||||
|
||||
(st/emit! (rt/assign-exception error)))
|
||||
|
||||
(defmethod ptk/handle-error :wasm-exception
|
||||
[error]
|
||||
(when-let [cause (::instance error)]
|
||||
(let [prefix (or (:prefix error) "Exception")]
|
||||
(ex/print-throwable cause :prefix prefix)))
|
||||
(st/emit! (rt/assign-exception error)))
|
||||
(ex/print-throwable cause)
|
||||
(let [code (get error :code)]
|
||||
(if (or (= code :panic)
|
||||
(= code :webgl-context-lost))
|
||||
(st/emit! (rt/assign-exception error))
|
||||
(flash :type :handled :cause cause)))))
|
||||
|
||||
;; We receive a explicit authentication error; If the uri is for
|
||||
;; workspace, dashboard, viewer or settings, then assign the exception
|
||||
@@ -373,22 +385,31 @@
|
||||
(.preventDefault ^js event)
|
||||
(when-let [cause (unchecked-get event "error")]
|
||||
(when-not (is-ignorable-exception? cause)
|
||||
(let [data (ex-data cause)
|
||||
type (get data :type)]
|
||||
(set! last-exception cause)
|
||||
(if (#{:wasm-critical :wasm-non-blocking :wasm-exception} type)
|
||||
(on-error cause)
|
||||
(do
|
||||
(ex/print-throwable cause :prefix "Uncaught Exception")
|
||||
(ts/schedule #(flash :cause cause :type :unhandled))))))))
|
||||
(if (stale-asset-error? cause)
|
||||
(cf/throttled-reload :reason (ex-message cause))
|
||||
(let [data (ex-data cause)
|
||||
type (get data :type)]
|
||||
(set! last-exception cause)
|
||||
(if (= :wasm-error type)
|
||||
(on-error cause)
|
||||
(do
|
||||
(ex/print-throwable cause :prefix "Uncaught Exception")
|
||||
(ts/asap #(flash :cause cause :type :unhandled)))))))))
|
||||
|
||||
(on-unhandled-rejection [event]
|
||||
(.preventDefault ^js event)
|
||||
(when-let [cause (unchecked-get event "reason")]
|
||||
(when-not (is-ignorable-exception? cause)
|
||||
(set! last-exception cause)
|
||||
(ex/print-throwable cause :prefix "Uncaught Rejection")
|
||||
(ts/schedule #(flash :cause cause :type :unhandled)))))]
|
||||
(if (stale-asset-error? cause)
|
||||
(cf/throttled-reload :reason (ex-message cause))
|
||||
(let [data (ex-data cause)
|
||||
type (get data :type)]
|
||||
(set! last-exception cause)
|
||||
(if (= :wasm-error type)
|
||||
(on-error cause)
|
||||
(do
|
||||
(ex/print-throwable cause :prefix "Uncaught Rejection")
|
||||
(ts/asap #(flash :cause cause :type :unhandled)))))))))]
|
||||
|
||||
(.addEventListener g/window "error" on-unhandled-error)
|
||||
(.addEventListener g/window "unhandledrejection" on-unhandled-rejection)
|
||||
|
||||
@@ -9,18 +9,17 @@
|
||||
(:require
|
||||
["react-error-boundary" :as reb]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.config :as cf]
|
||||
[app.main.errors :as errors]
|
||||
[app.main.refs :as refs]
|
||||
[goog.functions :as gfn]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc error-boundary*
|
||||
{::mf/props :obj}
|
||||
[{:keys [fallback children]}]
|
||||
(let [fallback-wrapper
|
||||
(mf/with-memo [fallback]
|
||||
(mf/fnc fallback-wrapper*
|
||||
{::mf/props :obj}
|
||||
[{:keys [error reset-error-boundary]}]
|
||||
(let [route (mf/deref refs/route)
|
||||
data (errors/exception->error-data error)]
|
||||
@@ -35,13 +34,19 @@
|
||||
;; very small amount of time, so we debounce for 100ms for
|
||||
;; avoid duplicate and redundant reports
|
||||
(gfn/debounce (fn [error info]
|
||||
(set! errors/last-exception error)
|
||||
(ex/print-throwable error)
|
||||
(js/console.error
|
||||
"Component trace: \n"
|
||||
(unchecked-get info "componentStack")
|
||||
"\n"
|
||||
error))
|
||||
;; If the error is a stale-asset error (cross-build
|
||||
;; module mismatch), force a hard page reload instead
|
||||
;; of showing the error page to the user.
|
||||
(if (errors/stale-asset-error? error)
|
||||
(cf/throttled-reload :reason (ex-message error))
|
||||
(do
|
||||
(set! errors/last-exception error)
|
||||
(ex/print-throwable error)
|
||||
(js/console.error
|
||||
"Component trace: \n"
|
||||
(unchecked-get info "componentStack")
|
||||
"\n"
|
||||
error))))
|
||||
100))]
|
||||
|
||||
[:> reb/ErrorBoundary
|
||||
|
||||
@@ -214,10 +214,11 @@
|
||||
(mf/use-effect
|
||||
deps
|
||||
(fn []
|
||||
(let [sub (->> stream (rx/subs! on-subscribe))]
|
||||
#(do
|
||||
(rx/dispose! sub)
|
||||
(when on-dispose (on-dispose))))))))
|
||||
(when stream
|
||||
(let [sub (->> stream (rx/subs! on-subscribe))]
|
||||
#(do
|
||||
(rx/dispose! sub)
|
||||
(when on-dispose (on-dispose)))))))))
|
||||
|
||||
;; https://reactjs.org/docs/hooks-faq.html#how-to-get-the-previous-props-or-state
|
||||
;; FIXME: replace with rumext
|
||||
|
||||
@@ -279,7 +279,7 @@
|
||||
(ev/event {::ev/name "enable-mcp"
|
||||
::ev/origin "integrations"
|
||||
:source "key-creation"}))
|
||||
(mbc/emit! :mcp-enabled-change true)
|
||||
(mbc/emit! :mcp-enabled-change-status true)
|
||||
(reset! created? true)))]
|
||||
|
||||
[:div {:class (stl/css :modal-overlay)}
|
||||
@@ -320,7 +320,7 @@
|
||||
(du/update-profile-props {:mcp-enabled true})
|
||||
(ev/event {::ev/name "regenerate-mcp-key"
|
||||
::ev/origin "integrations"}))
|
||||
(mbc/emit! :mcp-enabled-change true)
|
||||
(mbc/emit! :mcp-enabled-change-status true)
|
||||
(reset! created? true)))]
|
||||
|
||||
[:div {:class (stl/css :modal-overlay)}
|
||||
@@ -431,7 +431,7 @@
|
||||
(ev/event {::ev/name (if (true? value) "enable-mcp" "disable-mcp")
|
||||
::ev/origin "integrations"
|
||||
:source "toggle"}))
|
||||
(mbc/emit! :mcp-enabled-change value)))
|
||||
(mbc/emit! :mcp-enabled-change-status value)))
|
||||
|
||||
handle-generate-mcp-key
|
||||
(mf/use-fn
|
||||
@@ -449,7 +449,7 @@
|
||||
mdata {:on-success #(st/emit! (du/fetch-access-tokens))}]
|
||||
(st/emit! (du/delete-access-token (with-meta params mdata))
|
||||
(du/update-profile-props {:mcp-enabled false}))
|
||||
(mbc/emit! :mcp-enabled-change false))))
|
||||
(mbc/emit! :mcp-enabled-change-status false))))
|
||||
|
||||
on-copy-to-clipboard
|
||||
(mf/use-fn
|
||||
|
||||
@@ -477,8 +477,8 @@
|
||||
:service-unavailable
|
||||
[:> service-unavailable*]
|
||||
|
||||
:wasm-exception
|
||||
(case (get data :exception-type)
|
||||
:wasm-error
|
||||
(case (get data :code)
|
||||
:webgl-context-lost
|
||||
[:> webgl-context-lost*]
|
||||
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
[app.render-wasm.api :as wasm.api]
|
||||
[app.util.debug :as dbg]
|
||||
[app.util.text-editor :as ted]
|
||||
[app.util.timers :as ts]
|
||||
[beicon.v2.core :as rx]
|
||||
[promesa.core :as p]
|
||||
[rumext.v2 :as mf]))
|
||||
@@ -361,9 +362,9 @@
|
||||
|
||||
(mf/with-effect [focus]
|
||||
(when (and @canvas-init? @initialized?)
|
||||
(if (empty? focus)
|
||||
(wasm.api/clear-focus-mode)
|
||||
(wasm.api/set-focus-mode focus))))
|
||||
(ts/asap #(if (empty? focus)
|
||||
(wasm.api/clear-focus-mode)
|
||||
(wasm.api/set-focus-mode focus)))))
|
||||
|
||||
(mf/with-effect [vbox zoom]
|
||||
(when (and @canvas-init? initialized?)
|
||||
|
||||
@@ -53,6 +53,13 @@
|
||||
[rumext.v2 :as mf]))
|
||||
(def use-dpr? (contains? cf/flags :render-wasm-dpr))
|
||||
|
||||
(defn text-editor-wasm?
|
||||
[]
|
||||
(let [runtime-features (get @st/state :features-runtime)
|
||||
enabled-features (get @st/state :features)]
|
||||
(or (contains? runtime-features "text-editor-wasm/v1")
|
||||
(contains? enabled-features "text-editor-wasm/v1"))))
|
||||
|
||||
(def ^:const UUID-U8-SIZE 16)
|
||||
(def ^:const UUID-U32-SIZE (/ UUID-U8-SIZE 4))
|
||||
|
||||
@@ -149,11 +156,8 @@
|
||||
;; Determine if text-editor-wasm feature is active without requiring
|
||||
;; app.main.features to avoid circular dependency: check runtime and
|
||||
;; persisted feature sets in the store state.
|
||||
(let [runtime-features (get @st/state :features-runtime)
|
||||
enabled-features (get @st/state :features)]
|
||||
(when (or (contains? runtime-features "text-editor-wasm/v1")
|
||||
(contains? enabled-features "text-editor-wasm/v1"))
|
||||
(text-editor/text-editor-render-overlay)))
|
||||
(when (text-editor-wasm?)
|
||||
(text-editor/text-editor-render-overlay))
|
||||
;; Poll for editor events; if any event occurs, trigger a re-render
|
||||
(let [ev (text-editor/text-editor-poll-event)]
|
||||
(when (and ev (not= ev 0))
|
||||
@@ -1395,7 +1399,9 @@
|
||||
[]
|
||||
(cond-> 0
|
||||
(dbg/enabled? :wasm-viewbox)
|
||||
(bit-or 2r00000000000000000000000000000001)))
|
||||
(bit-or 2r00000000000000000000000000000001)
|
||||
(text-editor-wasm?)
|
||||
(bit-or 2r00000000000000000000000000001000)))
|
||||
|
||||
(defn set-canvas-size
|
||||
[canvas]
|
||||
@@ -1404,27 +1410,13 @@
|
||||
(set! (.-width canvas) (* dpr width))
|
||||
(set! (.-height canvas) (* dpr height))))
|
||||
|
||||
(defn- get-browser
|
||||
[]
|
||||
(when (exists? js/navigator)
|
||||
(let [user-agent (.-userAgent js/navigator)]
|
||||
(when user-agent
|
||||
(cond
|
||||
(re-find #"(?i)firefox" user-agent) :firefox
|
||||
(re-find #"(?i)chrome" user-agent) :chrome
|
||||
(re-find #"(?i)safari" user-agent) :safari
|
||||
(re-find #"(?i)edge" user-agent) :edge
|
||||
:else :unknown)))))
|
||||
|
||||
(defn- on-webgl-context-lost
|
||||
[event]
|
||||
(dom/prevent-default event)
|
||||
(reset! wasm/context-lost? true)
|
||||
(log/warn :hint "WebGL context lost")
|
||||
(ex/raise :type :wasm-exception
|
||||
:exception-type :webgl-context-lost
|
||||
:prefix "WebGL context lost"
|
||||
:hint "WebGL context lost"))
|
||||
(ex/raise :type :wasm-error
|
||||
:code :webgl-context-lost
|
||||
:hint "WASM Error: WebGL context lost"))
|
||||
|
||||
(defn init-canvas-context
|
||||
[canvas]
|
||||
@@ -1433,8 +1425,7 @@
|
||||
context-id (if (dbg/enabled? :wasm-gl-context-init-error) "fail" "webgl2")
|
||||
context (.getContext ^js canvas context-id default-context-options)
|
||||
context-init? (not (nil? context))
|
||||
browser (get-browser)
|
||||
browser (sr/translate-browser browser)]
|
||||
browser (sr/translate-browser cf/browser)]
|
||||
(when-not (nil? context)
|
||||
(let [handle (.registerContext ^js gl context #js {"majorVersion" 2})]
|
||||
(.makeContextCurrent ^js gl handle)
|
||||
|
||||
@@ -5,11 +5,13 @@
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.render-wasm.helpers
|
||||
#?(:cljs (:require-macros [app.render-wasm.helpers])))
|
||||
#?(:cljs (:require-macros [app.render-wasm.helpers]))
|
||||
(:require [app.common.data :as d]))
|
||||
|
||||
(def ^:export error-code
|
||||
(def error-code
|
||||
"WASM error code constants (must match render-wasm/src/error.rs and mem.rs)."
|
||||
{0x01 :wasm-non-blocking 0x02 :wasm-critical})
|
||||
{0x01 :non-blocking
|
||||
0x02 :panic})
|
||||
|
||||
(defmacro call
|
||||
"A helper for calling a wasm function.
|
||||
@@ -18,19 +20,19 @@
|
||||
- :wasm-non-blocking: call app.main.errors/on-error (eventually, shows a toast and logs the error)
|
||||
- :wasm-critical or unknown: throws an exception to be handled by the global error handler (eventually, shows the internal error page)"
|
||||
[module name & params]
|
||||
(let [fn-sym (with-meta (gensym "fn-") {:tag 'function})
|
||||
e-sym (gensym "e")
|
||||
code-sym (gensym "code")]
|
||||
(let [fn-sym (with-meta (gensym "fn-") {:tag 'function})
|
||||
cause-sym (gensym "cause")]
|
||||
`(let [~fn-sym (cljs.core/unchecked-get ~module ~name)]
|
||||
(try
|
||||
(~fn-sym ~@params)
|
||||
(catch :default ~e-sym
|
||||
(let [read-code# (cljs.core/unchecked-get ~module "_read_error_code")
|
||||
~code-sym (when read-code# (read-code#))
|
||||
type# (or (get app.render-wasm.helpers/error-code ~code-sym) :wasm-critical)
|
||||
ex# (ex-info (str "WASM error (type: " type# ")")
|
||||
{:fn ~name :type type# :message (.-message ~e-sym) :error-code ~code-sym}
|
||||
~e-sym)]
|
||||
(if (= type# :wasm-non-blocking)
|
||||
(@~'app.main.store/on-error ex#)
|
||||
(throw ex#))))))))
|
||||
(catch :default ~cause-sym
|
||||
(let [read-code-fn# (cljs.core/unchecked-get ~module "_read_error_code")
|
||||
code-num# (when read-code-fn# (read-code-fn#))
|
||||
code# (get error-code code-num# :wasm-critical)
|
||||
hint# (str "WASM Error (" (d/name code#) ")")
|
||||
context# {:type :wasm-error
|
||||
:code code#
|
||||
:hint hint#
|
||||
:fn ~name}
|
||||
cause# (ex-info hint# context# ~cause-sym)]
|
||||
(throw cause#)))))))
|
||||
|
||||
@@ -271,6 +271,8 @@
|
||||
:firefox 0
|
||||
:chrome 1
|
||||
:safari 2
|
||||
:safari-16 2
|
||||
:safari-17 2
|
||||
:edge 3
|
||||
:unknown 4
|
||||
4))
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
(defonce context-initialized? false)
|
||||
(defonce context-lost? (atom false))
|
||||
|
||||
|
||||
(defonce serializers
|
||||
#js {:blur-type shared/RawBlurType
|
||||
:blend-mode shared/RawBlendMode
|
||||
|
||||
@@ -17,10 +17,10 @@
|
||||
;; Using ex/ignoring because can receive a DOMException like this when
|
||||
;; importing the code as a library: Failed to read the 'localStorage'
|
||||
;; property from 'Window': Storage is disabled inside 'data:' URLs.
|
||||
(defonce ^:private local-storage-backend
|
||||
(defonce local-storage
|
||||
(ex/ignoring (unchecked-get g/global "localStorage")))
|
||||
|
||||
(defonce ^:private session-storage-backend
|
||||
(defonce session-storage
|
||||
(ex/ignoring (unchecked-get g/global "sessionStorage")))
|
||||
|
||||
(def ^:dynamic *sync*
|
||||
@@ -69,6 +69,17 @@
|
||||
(persistent! result))))
|
||||
{}))
|
||||
|
||||
(defn set-item
|
||||
[storage key val]
|
||||
(when (and (some? storage)
|
||||
(string? key))
|
||||
(.setItem ^js storage key val)))
|
||||
|
||||
(defn get-item
|
||||
[storage key]
|
||||
(when (some? storage)
|
||||
(.getItem storage key)))
|
||||
|
||||
(defn create-storage
|
||||
[backend prefix]
|
||||
(let [initial (load-data backend prefix)
|
||||
@@ -154,10 +165,10 @@
|
||||
(-remove-watch [_ key]
|
||||
(.delete watches key)))))
|
||||
|
||||
(defonce global (create-storage local-storage-backend "penpot-global"))
|
||||
(defonce user (create-storage local-storage-backend "penpot-user"))
|
||||
(defonce storage (create-storage local-storage-backend "penpot"))
|
||||
(defonce session (create-storage session-storage-backend "penpot"))
|
||||
(defonce global (create-storage local-storage "penpot-global"))
|
||||
(defonce user (create-storage local-storage "penpot-user"))
|
||||
(defonce storage (create-storage local-storage "penpot"))
|
||||
(defonce session (create-storage session-storage "penpot"))
|
||||
|
||||
(defonce before-unload
|
||||
(letfn [(on-before-unload [_]
|
||||
|
||||
@@ -7,8 +7,7 @@
|
||||
(ns app.util.timers
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[beicon.v2.core :as rx]
|
||||
[promesa.core :as p]))
|
||||
[beicon.v2.core :as rx]))
|
||||
|
||||
(defn schedule
|
||||
([func]
|
||||
@@ -30,8 +29,8 @@
|
||||
|
||||
(defn asap
|
||||
[f]
|
||||
(-> (p/resolved nil)
|
||||
(p/then (fn [_] (f)))))
|
||||
(-> (js/Promise.resolve nil)
|
||||
(.then (fn [_] (f)))))
|
||||
|
||||
(defn interval
|
||||
[ms func]
|
||||
|
||||
@@ -2514,6 +2514,9 @@ msgstr "Director"
|
||||
msgid "labels.discard"
|
||||
msgstr "Discard"
|
||||
|
||||
msgid "labels.dismiss"
|
||||
msgstr "Dismiss"
|
||||
|
||||
#: src/app/main/ui/settings/feedback.cljs:134, src/app/main/ui/static.cljs:409
|
||||
msgid "labels.download"
|
||||
msgstr "Download %s"
|
||||
@@ -3830,6 +3833,9 @@ msgstr "Invitation sent successfully"
|
||||
msgid "notifications.invitation-link-copied"
|
||||
msgstr "Invitation link copied"
|
||||
|
||||
msgid "notifications.mcp.active-tab-switching.text"
|
||||
msgstr "MCP is active in another tab. Switch here?"
|
||||
|
||||
#: src/app/main/ui/settings/delete_account.cljs:24
|
||||
msgid "notifications.profile-deletion-not-allowed"
|
||||
msgstr "You can't delete your profile. Reassign your teams before proceed."
|
||||
|
||||
@@ -2471,6 +2471,9 @@ msgstr "Director"
|
||||
msgid "labels.discard"
|
||||
msgstr "Descartar"
|
||||
|
||||
msgid "labels.dismiss"
|
||||
msgstr "Cancelar"
|
||||
|
||||
#: src/app/main/ui/settings/feedback.cljs:134, src/app/main/ui/static.cljs:409
|
||||
msgid "labels.download"
|
||||
msgstr "Descargar %s"
|
||||
@@ -3787,6 +3790,9 @@ msgstr "Invitación enviada con éxito"
|
||||
msgid "notifications.invitation-link-copied"
|
||||
msgstr "Enlace de invitacion copiado"
|
||||
|
||||
msgid "notifications.mcp.active-tab-switching.text"
|
||||
msgstr "MCP está activo en otra pestaña. ¿Cambiar a esta?"
|
||||
|
||||
#: src/app/main/ui/settings/delete_account.cljs:24
|
||||
msgid "notifications.profile-deletion-not-allowed"
|
||||
msgstr "No puedes borrar tu perfil. Reasigna tus equipos antes de seguir."
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pub const DEBUG_VISIBLE: u32 = 0x01;
|
||||
pub const PROFILE_REBUILD_TILES: u32 = 0x02;
|
||||
pub const FAST_MODE: u32 = 0x04;
|
||||
pub const INFO_TEXT: u32 = 0x08;
|
||||
|
||||
@@ -46,16 +46,25 @@ pub fn render_wasm_label(render_state: &mut RenderState) {
|
||||
let mut paint = skia::Paint::default();
|
||||
paint.set_color(skia::Color::GRAY);
|
||||
|
||||
let str = if render_state.options.is_debug_visible() {
|
||||
let mut str = if render_state.options.is_debug_visible() {
|
||||
"WASM RENDERER (DEBUG)"
|
||||
} else {
|
||||
"WASM RENDERER"
|
||||
};
|
||||
let (scalar, _) = render_state.fonts.debug_font().measure_str(str, None);
|
||||
let p = skia::Point::new(width as f32 - 25.0 - scalar, height as f32 - 25.0);
|
||||
let mut p = skia::Point::new(width as f32 - 25.0 - scalar, height as f32 - 25.0);
|
||||
|
||||
let debug_font = render_state.fonts.debug_font();
|
||||
canvas.draw_str(str, p, debug_font, &paint);
|
||||
|
||||
if render_state.options.show_info_text() {
|
||||
str = "TEXT EDITOR / V3";
|
||||
|
||||
let (scalar, _) = render_state.fonts.debug_font().measure_str(str, None);
|
||||
p.x = width as f32 - 25.0 - scalar;
|
||||
p.y -= 20.0;
|
||||
canvas.draw_str(str, p, debug_font, &paint);
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
|
||||
@@ -31,4 +31,8 @@ impl RenderOptions {
|
||||
pub fn dpr(&self) -> f32 {
|
||||
self.dpr.unwrap_or(1.0)
|
||||
}
|
||||
|
||||
pub fn show_info_text(&self) -> bool {
|
||||
self.flags & options::INFO_TEXT == options::INFO_TEXT
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user