Files
penpot/frontend/src/app/main/features.cljs
2024-11-05 13:16:56 +01:00

125 lines
4.4 KiB
Clojure

;; 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.main.features
"A thin, frontend centric abstraction layer and collection of
helpers for `app.common.features` namespace."
(:require
[app.common.features :as cfeat]
[app.common.logging :as log]
[app.config :as cf]
[app.main.store :as st]
[beicon.v2.core :as rx]
[clojure.set :as set]
[cuerdas.core :as str]
[okulary.core :as l]
[potok.v2.core :as ptk]
[rumext.v2 :as mf]))
(log/set-level! :trace)
(def global-enabled-features
(cfeat/get-enabled-features cf/flags))
(defn get-enabled-features
[state]
(-> (get state :features/runtime #{})
(set/intersection cfeat/no-migration-features)
(set/union global-enabled-features)))
(defn get-team-enabled-features
[state]
(-> global-enabled-features
(set/union (:features/runtime state #{}))
(set/intersection cfeat/no-migration-features)
(set/union (:features/team state #{}))))
(def features-ref
(l/derived get-team-enabled-features st/state =))
(defn active-feature?
"Given a state and feature, check if feature is enabled"
[state feature]
(assert (contains? cfeat/supported-features feature) "not supported feature")
(or (contains? (get state :features/runtime) feature)
(if (contains? cfeat/no-migration-features feature)
(or (contains? global-enabled-features feature)
(contains? (get state :features/team) feature))
(contains? (get state :features/team state) feature))))
(defn use-feature
"A react hook that checks if feature is currently enabled"
[feature]
(assert (contains? cfeat/supported-features feature) "Not supported feature")
(let [enabled-features (mf/deref features-ref)]
(contains? enabled-features feature)))
(defn toggle-feature
"An event constructor for runtime feature toggle.
Warning: if a feature is active globally or by team, it can't be
disabled."
[feature]
(ptk/reify ::toggle-feature
ptk/UpdateEvent
(update [_ state]
(assert (contains? cfeat/supported-features feature) "not supported feature")
(update state :features/runtime (fn [features]
(if (contains? features feature)
(do
(log/trc :hint "feature disabled" :feature feature)
(disj features feature))
(do
(log/trc :hint "feature enabled" :feature feature)
(conj features feature))))))))
(defn enable-feature
[feature]
(ptk/reify ::enable-feature
ptk/UpdateEvent
(update [_ state]
(assert (contains? cfeat/supported-features feature) "not supported feature")
(if (active-feature? state feature)
state
(do
(log/trc :hint "feature enabled" :feature feature)
(update state :features/runtime (fnil conj #{}) feature))))))
(defn initialize
([] (initialize #{}))
([team-features]
(assert (set? team-features) "expected a set of features")
(assert (every? string? team-features) "expected a set of strings")
(ptk/reify ::initialize
ptk/UpdateEvent
(update [_ state]
(let [runtime-features (get state :features/runtime #{})
team-features (into #{}
cfeat/xf-supported-features
team-features)]
(-> state
(assoc :features/runtime runtime-features)
(assoc :features/team team-features))))
ptk/WatchEvent
(watch [_ _ _]
(when *assert*
(->> (rx/from cfeat/no-migration-features)
;; text editor v2 isn't enabled by default even in devenv
(rx/filter #(not (or (contains? cfeat/backend-only-features %)
(= "text-editor/v2" %)
(= "design-tokens/v1" %))))
(rx/observe-on :async)
(rx/map enable-feature))))
ptk/EffectEvent
(effect [_ state _]
(log/trc :hint "initialized features"
:team (str/join "," (:features/team state))
:runtime (str/join "," (:features/runtime state)))))))