mirror of
https://github.com/penpot/penpot.git
synced 2026-07-31 18:36:18 +00:00
♻️ Normalize organization naming across Penpot (#10977)
This commit is contained in:
parent
a2231a8bf9
commit
94f51afb20
@ -769,13 +769,13 @@
|
||||
[value]
|
||||
(when-not (str/blank? value) value))
|
||||
|
||||
(defn org-sso-discovery-uri
|
||||
"Return the OIDC discovery URI from an org SSO config."
|
||||
(defn organization-sso-discovery-uri
|
||||
"Return the OIDC discovery URI from an organization SSO config."
|
||||
[sso]
|
||||
(non-blank-uri (:issuer sso)))
|
||||
|
||||
(defn prepare-org-sso-provider
|
||||
"Build an OIDC provider map dynamically from the Nitrate org SSO config.
|
||||
(defn prepare-organization-sso-provider
|
||||
"Build an OIDC provider map dynamically from the Nitrate organization SSO config.
|
||||
Uses OIDC discovery via :issuer when token/auth/user URIs are absent."
|
||||
[cfg {:keys [client-id client-secret issuer]}]
|
||||
(prepare-oidc-provider cfg
|
||||
@ -788,18 +788,18 @@
|
||||
:scopes default-oidc-scopes
|
||||
:skip-ssrf-check? true}))
|
||||
|
||||
(defn build-org-sso-auth-redirect-uri
|
||||
(defn build-organization-sso-auth-redirect-uri
|
||||
"Build the OIDC authorization redirect URI for an organization SSO config.
|
||||
Raises if the config is incomplete or OIDC discovery fails."
|
||||
[cfg sso & {:keys [dest-url organization-id provider]}]
|
||||
(let [organization-id (or organization-id (:organization-id sso))
|
||||
issuer (org-sso-discovery-uri sso)
|
||||
issuer (organization-sso-discovery-uri sso)
|
||||
dest-url (or dest-url (str (cf/get :public-uri)))]
|
||||
(when-not issuer
|
||||
(ex/raise :type :validation
|
||||
:code :invalid-sso-config
|
||||
:hint "missing issuer"))
|
||||
(let [oidc-provider (or provider (prepare-org-sso-provider cfg sso))
|
||||
(let [oidc-provider (or provider (prepare-organization-sso-provider cfg sso))
|
||||
state-token (tokens/generate cfg {:iss "oidc"
|
||||
:dest-url dest-url
|
||||
:organization-id organization-id
|
||||
@ -839,7 +839,7 @@
|
||||
(and (= error "access_denied")
|
||||
(str/includes? description "unauthorized")))))
|
||||
|
||||
(defn- probe-org-sso-client-credentials
|
||||
(defn- probe-organization-sso-client-credentials
|
||||
"Probe the token endpoint with a dummy authorization code.
|
||||
Valid client credentials are expected to answer with `invalid_grant`."
|
||||
[cfg provider]
|
||||
@ -864,10 +864,10 @@
|
||||
and the client credentials are accepted by the token endpoint."
|
||||
[cfg sso]
|
||||
(try
|
||||
(if (org-sso-discovery-uri sso)
|
||||
(let [provider (prepare-org-sso-provider cfg sso)]
|
||||
(and (build-org-sso-auth-redirect-uri cfg sso :provider provider)
|
||||
(probe-org-sso-client-credentials cfg provider)))
|
||||
(if (organization-sso-discovery-uri sso)
|
||||
(let [provider (prepare-organization-sso-provider cfg sso)]
|
||||
(and (build-organization-sso-auth-redirect-uri cfg sso :provider provider)
|
||||
(probe-organization-sso-client-credentials cfg provider)))
|
||||
false)
|
||||
(catch Throwable _ false)))
|
||||
|
||||
@ -897,12 +897,12 @@
|
||||
state (get params :state)
|
||||
state (tokens/verify cfg {:token state :iss "oidc"})]
|
||||
|
||||
;; Org SSO flow: state carries :dest-url — exchange the authorization
|
||||
;; Organization SSO flow: state carries :dest-url — exchange the authorization
|
||||
;; code with the OIDC provider to verify authentication actually occurred.
|
||||
(if-let [dest-url (:dest-url state)]
|
||||
(let [organization-id (:organization-id state)
|
||||
sso (nitrate/call cfg :get-org-sso {:organization-id organization-id})
|
||||
provider (prepare-org-sso-provider cfg sso)
|
||||
sso (nitrate/call cfg :get-organization-sso {:organization-id organization-id})
|
||||
provider (prepare-organization-sso-provider cfg sso)
|
||||
info (get-info cfg provider state code)
|
||||
session (session/get-session request)
|
||||
exp (or (:sso-token-exp info) (ct/in-future {:hours 48}))]
|
||||
|
||||
@ -440,18 +440,18 @@
|
||||
:id ::invite-to-team
|
||||
:schema schema:invite-to-team))
|
||||
|
||||
(def ^:private schema:invite-to-org
|
||||
(def ^:private schema:invite-to-organization
|
||||
[:map
|
||||
[:invited-by ::sm/text]
|
||||
[:user-name [:maybe ::sm/text]]
|
||||
[:token ::sm/text]
|
||||
[:organization schema:organization-data]])
|
||||
|
||||
(def invite-to-org
|
||||
"Org member invitation email."
|
||||
(def invite-to-organization
|
||||
"Organization member invitation email."
|
||||
(template-factory
|
||||
:id ::invite-to-org
|
||||
:schema schema:invite-to-org))
|
||||
:id ::invite-to-organization
|
||||
:schema schema:invite-to-organization))
|
||||
|
||||
(def ^:private schema:organization-setup-sso
|
||||
[:map
|
||||
|
||||
@ -226,19 +226,19 @@
|
||||
(-> (db/exec-one! cfg [sql (:profile-id session) (:id session)])
|
||||
(db/get-update-count))))
|
||||
|
||||
(def ^:private sql:clear-org-sso-sessions
|
||||
(def ^:private sql:clear-organization-sso-sessions
|
||||
(str "UPDATE http_session_v2 "
|
||||
"SET props = props #- ARRAY['~:sso', ?]::text[] "
|
||||
"WHERE props IS NOT NULL "
|
||||
"AND jsonb_exists(props -> '~:sso', ?)"))
|
||||
|
||||
(defn clear-org-sso-sessions!
|
||||
(defn clear-organization-sso-sessions!
|
||||
"Remove the SSO entry for organization-id from the props of every
|
||||
session that currently holds it. The key is transit-encoded as the
|
||||
string '~u<uuid>' under the '~:sso' path."
|
||||
[pool organization-id]
|
||||
(let [org-key (str "~u" organization-id)]
|
||||
(db/exec! pool [sql:clear-org-sso-sessions org-key org-key])))
|
||||
(let [organization-key (str "~u" organization-id)]
|
||||
(db/exec! pool [sql:clear-organization-sso-sessions organization-key organization-key])))
|
||||
|
||||
(defn- renew-session?
|
||||
[{:keys [id modified-at] :as session}]
|
||||
|
||||
@ -162,7 +162,7 @@
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def ^:private schema:org-summary
|
||||
(def ^:private schema:organization-summary
|
||||
[:map
|
||||
[:id ::sm/uuid]
|
||||
[:name ::sm/text]
|
||||
@ -189,7 +189,7 @@
|
||||
:decode/json ct/inst
|
||||
:encode/json inst-ms}}))
|
||||
|
||||
(def ^:private schema:profile-org
|
||||
(def ^:private schema:profile-organization
|
||||
[:map
|
||||
[:is-member :boolean]
|
||||
[:organization-id {:optional true} [:maybe ::sm/uuid]]
|
||||
@ -253,13 +253,13 @@
|
||||
[:map
|
||||
[:licenses ::sm/boolean]])
|
||||
|
||||
(defn- get-team-org-api
|
||||
(defn- get-team-organization-api
|
||||
[cfg {:keys [team-id] :as params}]
|
||||
(request-to-nitrate cfg :get
|
||||
(generate-nitrate-uri "api/teams/" team-id)
|
||||
cto/schema:team-with-organization params))
|
||||
|
||||
(defn- get-org-membership-api
|
||||
(defn- get-organization-membership-api
|
||||
[cfg {:keys [profile-id organization-id] :as params}]
|
||||
(request-to-nitrate cfg :get
|
||||
(generate-nitrate-uri
|
||||
@ -267,9 +267,9 @@
|
||||
organization-id
|
||||
"members/"
|
||||
profile-id)
|
||||
schema:profile-org params))
|
||||
schema:profile-organization params))
|
||||
|
||||
(defn- get-org-membership-by-team-api
|
||||
(defn- get-organization-membership-by-team-api
|
||||
[cfg {:keys [profile-id team-id] :as params}]
|
||||
(request-to-nitrate cfg :get
|
||||
(generate-nitrate-uri
|
||||
@ -277,28 +277,28 @@
|
||||
team-id
|
||||
"users/"
|
||||
profile-id)
|
||||
schema:profile-org params))
|
||||
schema:profile-organization params))
|
||||
|
||||
(defn- get-org-summary-api
|
||||
(defn- get-organization-summary-api
|
||||
[cfg {:keys [organization-id] :as params}]
|
||||
(request-to-nitrate cfg :get
|
||||
(generate-nitrate-uri
|
||||
"api/organizations/"
|
||||
organization-id
|
||||
"summary")
|
||||
schema:org-summary params))
|
||||
schema:organization-summary params))
|
||||
|
||||
(defn- get-owned-orgs-api
|
||||
(defn- get-owned-organizations-api
|
||||
[cfg {:keys [profile-id] :as params}]
|
||||
(request-to-nitrate cfg :get
|
||||
(generate-nitrate-uri
|
||||
"api/users/"
|
||||
profile-id
|
||||
"owned-organizations")
|
||||
[:vector schema:org-summary]
|
||||
[:vector schema:organization-summary]
|
||||
params))
|
||||
|
||||
(def ^:private schema:org-summary-counts
|
||||
(def ^:private schema:organization-summary-counts
|
||||
[:map
|
||||
[:id ::sm/uuid]
|
||||
[:name ::sm/text]
|
||||
@ -308,20 +308,20 @@
|
||||
[:avatar-bg-url {:optional true} [:maybe ::sm/uri]]
|
||||
[:logo-id {:optional true} [:maybe ::sm/uuid]]])
|
||||
|
||||
(defn- get-owned-orgs-summary-api
|
||||
(defn- get-owned-organizations-summary-api
|
||||
[cfg {:keys [profile-id] :as params}]
|
||||
(let [orgs (request-to-nitrate cfg :get
|
||||
(generate-nitrate-uri
|
||||
"api/users/"
|
||||
profile-id
|
||||
"owned-organizations-summary")
|
||||
[:vector schema:org-summary-counts]
|
||||
params)]
|
||||
(mapv (fn [org]
|
||||
(if-let [logo-id (:logo-id org)]
|
||||
(assoc org :custom-photo (generate-public-uri "assets/by-id/" logo-id))
|
||||
org))
|
||||
orgs)))
|
||||
(let [organizations (request-to-nitrate cfg :get
|
||||
(generate-nitrate-uri
|
||||
"api/users/"
|
||||
profile-id
|
||||
"owned-organizations-summary")
|
||||
[:vector schema:organization-summary-counts]
|
||||
params)]
|
||||
(mapv (fn [organization]
|
||||
(if-let [logo-id (:logo-id organization)]
|
||||
(assoc organization :custom-photo (generate-public-uri "assets/by-id/" logo-id))
|
||||
organization))
|
||||
organizations)))
|
||||
|
||||
(defn- cleanup-deleted-penpot-user-api
|
||||
[cfg {:keys [profile-id] :as params}]
|
||||
@ -332,7 +332,7 @@
|
||||
"cleanup-after-deletion")
|
||||
nil params))
|
||||
|
||||
(defn- set-team-org-api
|
||||
(defn- set-team-organization-api
|
||||
[cfg {:keys [organization-id team-id is-default] :as params}]
|
||||
(let [params (assoc params :request-params {:team-id team-id
|
||||
:is-your-penpot (true? is-default)})
|
||||
@ -348,7 +348,7 @@
|
||||
custom-photo
|
||||
(assoc-in [:organization :custom-photo] custom-photo))))
|
||||
|
||||
(defn- add-profile-to-org-api
|
||||
(defn- add-profile-to-organization-api
|
||||
[cfg {:keys [profile-id organization-id team-id email] :as params}]
|
||||
(let [request-params (cond-> {:user-id profile-id :team-id team-id}
|
||||
(some? email) (assoc :email email))
|
||||
@ -358,9 +358,9 @@
|
||||
"api/organizations/"
|
||||
organization-id
|
||||
"add-user")
|
||||
schema:profile-org params)))
|
||||
schema:profile-organization params)))
|
||||
|
||||
(defn- remove-profile-from-org-api
|
||||
(defn- remove-profile-from-organization-api
|
||||
[cfg {:keys [profile-id organization-id user-who-delete-member deleted-by-role] :as params}]
|
||||
(let [request-params (cond-> {:user-id profile-id}
|
||||
(some? user-who-delete-member)
|
||||
@ -375,7 +375,7 @@
|
||||
"remove-user")
|
||||
nil params)))
|
||||
|
||||
(defn- remove-team-from-org-api
|
||||
(defn- remove-team-from-organization-api
|
||||
[cfg {:keys [team-id organization-id] :as params}]
|
||||
(let [params (assoc params :request-params {:team-id team-id})]
|
||||
(request-to-nitrate cfg :post
|
||||
@ -433,7 +433,7 @@
|
||||
[:map
|
||||
[:cancel-at [:maybe schema:timestamp]]])
|
||||
|
||||
(defn- get-org-permissions-api
|
||||
(defn- get-organization-permissions-api
|
||||
[cfg {:keys [organization-id] :as params}]
|
||||
(request-to-nitrate cfg :get
|
||||
(generate-nitrate-uri
|
||||
@ -446,7 +446,7 @@
|
||||
[:permissions [:map-of :keyword :string]]]
|
||||
params))
|
||||
|
||||
(defn- get-org-sso-api
|
||||
(defn- get-organization-sso-api
|
||||
"Fetches the SSO configuration for an organization from Nitrate."
|
||||
[cfg {:keys [organization-id] :as params}]
|
||||
(request-to-nitrate cfg :get
|
||||
@ -457,14 +457,14 @@
|
||||
schema:nitrate-sso
|
||||
params))
|
||||
|
||||
(defn- get-org-sso-by-team-api
|
||||
(defn- get-organization-sso-by-team-api
|
||||
[cfg {:keys [team-id] :as params}]
|
||||
(request-to-nitrate cfg :get
|
||||
(generate-nitrate-uri "api/teams/" team-id "sso")
|
||||
schema:nitrate-sso
|
||||
params))
|
||||
|
||||
(defn- get-org-members-api
|
||||
(defn- get-organization-members-api
|
||||
[cfg {:keys [organization-id] :as params}]
|
||||
(request-to-nitrate cfg :get
|
||||
(generate-nitrate-uri
|
||||
@ -488,22 +488,22 @@
|
||||
(defmethod ig/init-key ::client
|
||||
[_ cfg]
|
||||
(when (contains? cf/flags :nitrate)
|
||||
{:get-team-org (partial get-team-org-api cfg)
|
||||
:set-team-org (partial set-team-org-api cfg)
|
||||
:get-org-membership (partial get-org-membership-api cfg)
|
||||
:get-org-membership-by-team (partial get-org-membership-by-team-api cfg)
|
||||
:get-org-summary (partial get-org-summary-api cfg)
|
||||
:get-owned-orgs (partial get-owned-orgs-api cfg)
|
||||
:get-owned-orgs-summary (partial get-owned-orgs-summary-api cfg)
|
||||
:get-org-members (partial get-org-members-api cfg)
|
||||
{:get-team-organization (partial get-team-organization-api cfg)
|
||||
:set-team-organization (partial set-team-organization-api cfg)
|
||||
:get-organization-membership (partial get-organization-membership-api cfg)
|
||||
:get-organization-membership-by-team (partial get-organization-membership-by-team-api cfg)
|
||||
:get-organization-summary (partial get-organization-summary-api cfg)
|
||||
:get-owned-organizations (partial get-owned-organizations-api cfg)
|
||||
:get-owned-organizations-summary (partial get-owned-organizations-summary-api cfg)
|
||||
:get-organization-members (partial get-organization-members-api cfg)
|
||||
:cleanup-deleted-penpot-user (partial cleanup-deleted-penpot-user-api cfg)
|
||||
:add-profile-to-org (partial add-profile-to-org-api cfg)
|
||||
:remove-profile-from-org (partial remove-profile-from-org-api cfg)
|
||||
:get-org-permissions (partial get-org-permissions-api cfg)
|
||||
:get-org-sso-by-team (partial get-org-sso-by-team-api cfg)
|
||||
:get-org-sso (partial get-org-sso-api cfg)
|
||||
:add-profile-to-organization (partial add-profile-to-organization-api cfg)
|
||||
:remove-profile-from-organization (partial remove-profile-from-organization-api cfg)
|
||||
:get-organization-permissions (partial get-organization-permissions-api cfg)
|
||||
:get-organization-sso-by-team (partial get-organization-sso-by-team-api cfg)
|
||||
:get-organization-sso (partial get-organization-sso-api cfg)
|
||||
:delete-team (partial delete-team-api cfg)
|
||||
:remove-team-from-org (partial remove-team-from-org-api cfg)
|
||||
:remove-team-from-organization (partial remove-team-from-organization-api cfg)
|
||||
:get-subscription (partial get-subscription-api cfg)
|
||||
:get-subscription-warning (partial get-subscription-warning-api cfg)
|
||||
:connectivity (partial get-connectivity-api cfg)
|
||||
@ -514,9 +514,9 @@
|
||||
;; UTILS
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defonce ^:private team-org-owner-cache
|
||||
(defonce ^:private team-organization-owner-cache
|
||||
;; Short TTL: permission checks run on the read path, so we avoid an
|
||||
;; HTTP call to nitrate per check. The org owner of a team rarely
|
||||
;; HTTP call to nitrate per check. The organization owner of a team rarely
|
||||
;; changes, and stale entries only grant read access for a few seconds.
|
||||
(cache/create :expire "30s" :max-size 2048))
|
||||
|
||||
@ -526,24 +526,24 @@
|
||||
[cfg]
|
||||
(and (map? cfg) (some? (get cfg ::client))))
|
||||
|
||||
(def ^:private cache-miss ::no-org-owner)
|
||||
(def ^:private cache-miss ::no-organization-owner)
|
||||
|
||||
(defn- get-team-org-owner-id
|
||||
(defn- get-team-organization-owner-id
|
||||
"Returns the organization owner-id for `team-id`, or nil. Cached
|
||||
briefly, including negative results (teams with no organization) so
|
||||
repeated unauthorized probes don't each hit nitrate."
|
||||
[cfg team-id]
|
||||
(let [owner-id (cache/get team-org-owner-cache team-id
|
||||
(let [owner-id (cache/get team-organization-owner-cache team-id
|
||||
(fn [team-id]
|
||||
(let [team-with-org (call cfg :get-team-org {:team-id team-id})]
|
||||
(or (get-in team-with-org [:organization :owner-id])
|
||||
(let [team-with-organization (call cfg :get-team-organization {:team-id team-id})]
|
||||
(or (get-in team-with-organization [:organization :owner-id])
|
||||
cache-miss))))]
|
||||
(when-not (= owner-id cache-miss)
|
||||
owner-id)))
|
||||
|
||||
(defn organization-owner-of-team?
|
||||
"True if `profile-id` is the owner of the organization that owns
|
||||
`team-id`. Used to grant non-member org owners read-only access to the
|
||||
`team-id`. Used to grant non-member organization owners read-only access to the
|
||||
teams of their organizations. `cfg` must be a config map with the
|
||||
nitrate client; raw db connections/pools yield false so internal
|
||||
callers are unaffected. Returns false when the :nitrate flag is off."
|
||||
@ -553,17 +553,17 @@
|
||||
(nitrate-client? cfg)
|
||||
(some? team-id)
|
||||
(some? profile-id))
|
||||
(= profile-id (get-team-org-owner-id cfg team-id)))))
|
||||
(= profile-id (get-team-organization-owner-id cfg team-id)))))
|
||||
|
||||
(defn sso-session-authorized?
|
||||
"Fetches the org-SSO config for the given organization or team and checks
|
||||
"Fetches the organization-SSO config for the given organization or team and checks
|
||||
whether the HTTP request has a valid session entry for it. Returns a map
|
||||
with :authorized and :sso keys."
|
||||
[cfg organization-id team-id request]
|
||||
(let [session (session/get-session request)
|
||||
sso (if organization-id
|
||||
(call cfg :get-org-sso {:organization-id organization-id})
|
||||
(call cfg :get-org-sso-by-team {:team-id team-id}))]
|
||||
(call cfg :get-organization-sso {:organization-id organization-id})
|
||||
(call cfg :get-organization-sso-by-team {:team-id team-id}))]
|
||||
(if-not (:active sso)
|
||||
{:authorized true :sso sso}
|
||||
(if-not (str/blank? (:issuer sso))
|
||||
@ -596,21 +596,21 @@
|
||||
:cause cause)
|
||||
profile)))))
|
||||
|
||||
(defn add-org-info-to-team
|
||||
(defn add-organization-info-to-team
|
||||
"Enriches a team map with organization information from Nitrate.
|
||||
Adds organization-id, organization-name, organization-slug, organization-owner-id, and your-penpot fields.
|
||||
Returns the original team unchanged if the request fails or org data is nil.
|
||||
Returns the original team unchanged if the request fails or organization data is nil.
|
||||
Propagates `:nitrate-unavailable` so the request is rejected when Nitrate is unreachable."
|
||||
[cfg team params]
|
||||
(try
|
||||
(let [params (assoc (or params {}) :team-id (:id team))
|
||||
team-with-org (call cfg :get-team-org params)
|
||||
org (:organization team-with-org)]
|
||||
(if (some? org)
|
||||
(-> (cto/apply-organization team (assoc org :custom-photo
|
||||
(when-let [logo-id (:logo-id org)]
|
||||
team-with-organization (call cfg :get-team-organization params)
|
||||
organization (:organization team-with-organization)]
|
||||
(if (some? organization)
|
||||
(-> (cto/apply-organization team (assoc organization :custom-photo
|
||||
(when-let [logo-id (:logo-id organization)]
|
||||
(generate-public-uri "assets/by-id/" logo-id))))
|
||||
(assoc :is-default (or (:is-default team) (true? (:is-your-penpot team-with-org)))))
|
||||
(assoc :is-default (or (:is-default team) (true? (:is-your-penpot team-with-organization)))))
|
||||
team))
|
||||
(catch Throwable cause
|
||||
(if (= :nitrate-unavailable (-> cause ex-data :type))
|
||||
@ -630,10 +630,10 @@
|
||||
:team-id (:id team)
|
||||
:organization-id (:organization-id params)
|
||||
:is-default (:is-default params))
|
||||
result (call cfg :set-team-org params)]
|
||||
result (call cfg :set-team-organization params)]
|
||||
(when (nil? result)
|
||||
(ex/raise :type :internal
|
||||
:code :failed-to-set-team-org
|
||||
:code :failed-to-set-team-organization
|
||||
:context {:team-id (:id team)
|
||||
:organization-id (:organization-id params)}))
|
||||
team))
|
||||
|
||||
@ -250,13 +250,13 @@
|
||||
f))
|
||||
|
||||
|
||||
(defonce ^:private org-sso-auth-cache
|
||||
(defonce ^:private organization-sso-auth-cache
|
||||
(cache/create :expire "15m" :max-size 1024))
|
||||
|
||||
(defn invalidate-org-sso-cache-by-org!
|
||||
"Invalidates all org-SSO authorization cache entries for the given organization-id."
|
||||
(defn invalidate-organization-sso-cache-by-organization!
|
||||
"Invalidates all organization-SSO authorization cache entries for the given organization-id."
|
||||
[organization-id]
|
||||
(cache/invalidate-if org-sso-auth-cache #(= (:organization-id %) organization-id)))
|
||||
(cache/invalidate-if organization-sso-auth-cache #(= (:organization-id %) organization-id)))
|
||||
|
||||
(defn- wrap-nitrate-sso
|
||||
"Enforce Nitrate organization SSO authentication for RPC handlers.
|
||||
@ -268,16 +268,16 @@
|
||||
4. Explicit :file-id param -> lookup file's team via join
|
||||
5. :id param dispatched by ::rpc/id-type metadata (:team, :project, or :file)
|
||||
|
||||
Once the context is resolved, checks if the user is authorized within that org's
|
||||
Once the context is resolved, checks if the user is authorized within that organization's
|
||||
SSO session using nitrate/sso-session-authorized?. Authorized results are cached
|
||||
by [profile-id cache-ref] for 15 minutes to avoid repeated lookups.
|
||||
|
||||
Only activates when:
|
||||
- Nitrate flag is enabled
|
||||
- Endpoint requires authentication (::auth true by default)
|
||||
- Endpoint is not marked with ::nitrate/org-sso false
|
||||
- Endpoint is not marked with ::nitrate/organization-sso false
|
||||
|
||||
Raises :nitrate-sso-required error if user is not authorized in the org."
|
||||
Raises :nitrate-sso-required error if user is not authorized in the organization."
|
||||
[_ f mdata]
|
||||
(if (and (contains? cf/flags :nitrate)
|
||||
(::auth mdata true) ;; only for endpoints that needs auth
|
||||
@ -299,7 +299,7 @@
|
||||
(let [cache-ref (or organization-id team-id project-id file-id)
|
||||
|
||||
cache-key [profile-id cache-ref]
|
||||
cached (cache/get org-sso-auth-cache cache-key)
|
||||
cached (cache/get organization-sso-auth-cache cache-key)
|
||||
result (if (some? cached)
|
||||
cached
|
||||
(let [team-id (when-not organization-id
|
||||
@ -314,7 +314,7 @@
|
||||
entry {:authorized authorized
|
||||
:organization-id (:organization-id sso)}]
|
||||
(when authorized
|
||||
(cache/get org-sso-auth-cache cache-key (constantly entry)))
|
||||
(cache/get organization-sso-auth-cache cache-key (constantly entry)))
|
||||
entry))]
|
||||
(if (:authorized result)
|
||||
(f cfg params)
|
||||
|
||||
@ -42,11 +42,11 @@
|
||||
:code :cant-move-default-team))))
|
||||
|
||||
(defn assert-membership [cfg profile-id organization-id]
|
||||
(let [membership (nitrate/call cfg :get-org-membership {:profile-id profile-id
|
||||
:organization-id organization-id})]
|
||||
(let [membership (nitrate/call cfg :get-organization-membership {:profile-id profile-id
|
||||
:organization-id organization-id})]
|
||||
(when-not (:organization-id membership)
|
||||
(ex/raise :type :validation
|
||||
:code :organization-doesnt-exists))
|
||||
:code :organization-does-not-exist))
|
||||
|
||||
(when-not (:is-member membership)
|
||||
(ex/raise :type :validation
|
||||
@ -181,7 +181,7 @@
|
||||
{})))
|
||||
{}))
|
||||
|
||||
(defn- build-leave-org-plan
|
||||
(defn- build-leave-organization-plan
|
||||
[{:keys [::db/conn]} default-team-id teams-to-delete keep-default-team-requested?]
|
||||
(let [all-teams (cond-> (set teams-to-delete) default-team-id (conj default-team-id))
|
||||
files-counts (get-team-files-counts conn all-teams)
|
||||
@ -194,18 +194,18 @@
|
||||
{:deletable-team-ids deletable
|
||||
:keep-default-team? keep-default?
|
||||
:delete-default-team? (boolean (and default-team-id (not keep-default?)))
|
||||
:detach-from-org-team-ids to-detach}))
|
||||
:detach-from-organization-team-ids to-detach}))
|
||||
|
||||
(defn get-leave-org-summary
|
||||
(defn get-leave-organization-summary
|
||||
[cfg default-team-id teams-to-delete teams-to-transfer-count teams-to-exit-count]
|
||||
(let [{:keys [deletable-team-ids detach-from-org-team-ids]}
|
||||
(build-leave-org-plan cfg default-team-id teams-to-delete nil)]
|
||||
(let [{:keys [deletable-team-ids detach-from-organization-team-ids]}
|
||||
(build-leave-organization-plan cfg default-team-id teams-to-delete nil)]
|
||||
{:teams-to-delete (count deletable-team-ids)
|
||||
:teams-to-transfer teams-to-transfer-count
|
||||
:teams-to-exit teams-to-exit-count
|
||||
:teams-to-detach (count detach-from-org-team-ids)}))
|
||||
:teams-to-detach (count detach-from-organization-team-ids)}))
|
||||
|
||||
(def ^:private schema:leave-org
|
||||
(def ^:private schema:leave-organization
|
||||
[:map
|
||||
[:id ::sm/uuid]
|
||||
[:name ::sm/text]
|
||||
@ -218,7 +218,7 @@
|
||||
[:id ::sm/uuid]
|
||||
[:reassign-to {:optional true} ::sm/uuid]]]]])
|
||||
|
||||
(def ^:private schema:get-leave-org-summary-result
|
||||
(def ^:private schema:get-leave-organization-summary-result
|
||||
[:map
|
||||
[:teams-to-delete ::sm/int]
|
||||
[:teams-to-transfer ::sm/int]
|
||||
@ -227,40 +227,40 @@
|
||||
[:member-added-at [:maybe ct/schema:inst]]
|
||||
[:organization-member-count-before ::sm/int]])
|
||||
|
||||
(def ^:private schema:get-leave-org-summary
|
||||
(def ^:private schema:get-leave-organization-summary
|
||||
[:map
|
||||
[:id ::sm/uuid]
|
||||
[:default-team-id ::sm/uuid]])
|
||||
|
||||
|
||||
(defn- get-organization-teams-for-user
|
||||
[{:keys [::db/conn] :as cfg} org-summary profile-id]
|
||||
(let [org-team-ids (->> (:teams org-summary)
|
||||
(map :id))
|
||||
ids-array (db/create-array conn "uuid" org-team-ids)]
|
||||
[{:keys [::db/conn] :as cfg} organization-summary profile-id]
|
||||
(let [organization-team-ids (->> (:teams organization-summary)
|
||||
(map :id))
|
||||
ids-array (db/create-array conn "uuid" organization-team-ids)]
|
||||
(db/exec! conn [sql:get-member-teams-info profile-id ids-array])))
|
||||
|
||||
(defn- calculate-valid-teams
|
||||
([org-teams default-team-id]
|
||||
([organization-teams default-team-id]
|
||||
(let [;; valid default team is the one which id is default-team-id
|
||||
valid-default-team (d/seek #(= default-team-id (:id %)) org-teams)
|
||||
valid-default-team (d/seek #(= default-team-id (:id %)) organization-teams)
|
||||
|
||||
;; Remove your-penpot for the rest of validations
|
||||
org-teams (remove #(= default-team-id (:id %)) org-teams)
|
||||
organization-teams (remove #(= default-team-id (:id %)) organization-teams)
|
||||
|
||||
;; valid teams to delete are those that the user is owner, and only have one member
|
||||
valid-teams-to-delete-ids (->> org-teams
|
||||
valid-teams-to-delete-ids (->> organization-teams
|
||||
(filter #(and (:is-owner %)
|
||||
(= (:num-members %) 1)))
|
||||
(map :id)
|
||||
(into #{}))
|
||||
;; valid teams to transfer are those that the user is owner, and have more than one member
|
||||
valid-teams-to-transfer (->> org-teams
|
||||
valid-teams-to-transfer (->> organization-teams
|
||||
(filter #(and (:is-owner %)
|
||||
(> (:num-members %) 1))))
|
||||
|
||||
;; valid teams to exit are those that the user isn't owner, and have more than one member
|
||||
valid-teams-to-exit (->> org-teams
|
||||
valid-teams-to-exit (->> organization-teams
|
||||
(filter #(and (not (:is-owner %))
|
||||
(> (:num-members %) 1))))]
|
||||
{:valid-teams-to-delete-ids valid-teams-to-delete-ids
|
||||
@ -269,17 +269,17 @@
|
||||
:valid-default-team valid-default-team})))
|
||||
|
||||
(defn get-valid-teams [cfg organization-id profile-id default-team-id]
|
||||
(let [org-summary (nitrate/call cfg :get-org-summary {:organization-id organization-id})
|
||||
org-teams (get-organization-teams-for-user cfg org-summary profile-id)]
|
||||
(calculate-valid-teams org-teams default-team-id)))
|
||||
(let [organization-summary (nitrate/call cfg :get-organization-summary {:organization-id organization-id})
|
||||
organization-teams (get-organization-teams-for-user cfg organization-summary profile-id)]
|
||||
(calculate-valid-teams organization-teams default-team-id)))
|
||||
|
||||
(defn- assert-valid-teams [cfg profile-id organization-id default-team-id teams-to-delete teams-to-leave]
|
||||
(let [org-summary (nitrate/call cfg :get-org-summary {:organization-id organization-id})
|
||||
org-teams (get-organization-teams-for-user cfg org-summary profile-id)
|
||||
(let [organization-summary (nitrate/call cfg :get-organization-summary {:organization-id organization-id})
|
||||
organization-teams (get-organization-teams-for-user cfg organization-summary profile-id)
|
||||
{:keys [valid-teams-to-delete-ids
|
||||
valid-teams-to-transfer
|
||||
valid-teams-to-exit
|
||||
valid-default-team]} (calculate-valid-teams org-teams default-team-id)
|
||||
valid-default-team]} (calculate-valid-teams organization-teams default-team-id)
|
||||
|
||||
|
||||
|
||||
@ -297,7 +297,7 @@
|
||||
;; - if it has a reassign-to, it belongs to valid-teams-to-transfer and
|
||||
;; the reassign-to is a member of the team and not the current user;
|
||||
;; - if it hasn't a reassign-to, check that it belongs to valid-teams-to-exit
|
||||
teams-by-id (d/index-by :id org-teams)
|
||||
teams-by-id (d/index-by :id organization-teams)
|
||||
valid-teams-to-leave? (and
|
||||
(= valid-teams-to-leave-ids (->> teams-to-leave (map :id) (into #{})))
|
||||
(every? (fn [{:keys [id reassign-to]}]
|
||||
@ -308,10 +308,10 @@
|
||||
(contains? members reassign-to)))
|
||||
(contains? valid-teams-to-exit-ids id)))
|
||||
teams-to-leave))]
|
||||
;; the org owner cannot leave
|
||||
(when (= (:owner-id org-summary) profile-id)
|
||||
;; the organization owner cannot leave
|
||||
(when (= (:owner-id organization-summary) profile-id)
|
||||
(ex/raise :type :validation
|
||||
:code :org-owner-cannot-leave))
|
||||
:code :organization-owner-cannot-leave))
|
||||
|
||||
(when (or
|
||||
(not valid-teams-to-delete?)
|
||||
@ -322,14 +322,14 @@
|
||||
|
||||
|
||||
|
||||
(defn leave-org
|
||||
(defn leave-organization
|
||||
[{:keys [::db/conn] :as cfg}
|
||||
{:keys [profile-id id name default-team-id teams-to-delete teams-to-leave skip-validation keep-default-team-requested?
|
||||
user-who-delete-member deleted-by-role]}]
|
||||
(let [org-prefix (str "[" (d/sanitize-string name) "] ")
|
||||
(let [organization-prefix (str "[" (d/sanitize-string name) "] ")
|
||||
{:keys [deletable-team-ids
|
||||
keep-default-team?
|
||||
detach-from-org-team-ids]} (build-leave-org-plan cfg default-team-id teams-to-delete keep-default-team-requested?)]
|
||||
detach-from-organization-team-ids]} (build-leave-organization-plan cfg default-team-id teams-to-delete keep-default-team-requested?)]
|
||||
|
||||
;; assert that the received teams are valid, checking the different constraints
|
||||
(when-not skip-validation
|
||||
@ -346,21 +346,21 @@
|
||||
(doseq [{:keys [id reassign-to]} teams-to-leave]
|
||||
(teams/leave-team cfg {:profile-id profile-id :id id :reassign-to reassign-to}))
|
||||
|
||||
;; Process org "Your Penpot" team: keep with prefix if needed, otherwise delete.
|
||||
;; Process organization "Your Penpot" team: keep with prefix if needed, otherwise delete.
|
||||
(when default-team-id
|
||||
(if keep-default-team?
|
||||
(db/exec! conn [sql:prefix-team-name-and-unset-default org-prefix default-team-id])
|
||||
(db/exec! conn [sql:prefix-team-name-and-unset-default organization-prefix default-team-id])
|
||||
(teams/delete-team cfg {:profile-id profile-id
|
||||
:team-id default-team-id})))
|
||||
|
||||
;; Detach retained owned teams from the organization in Nitrate.
|
||||
;; Nitrate will rehome them to its fallback/default org.
|
||||
(doseq [team-id detach-from-org-team-ids]
|
||||
(nitrate/call cfg :remove-team-from-org {:team-id team-id
|
||||
:organization-id id}))
|
||||
;; Nitrate will rehome them to its fallback/default organization.
|
||||
(doseq [team-id detach-from-organization-team-ids]
|
||||
(nitrate/call cfg :remove-team-from-organization {:team-id team-id
|
||||
:organization-id id}))
|
||||
|
||||
;; Api call to nitrate
|
||||
(nitrate/call cfg :remove-profile-from-org
|
||||
(nitrate/call cfg :remove-profile-from-organization
|
||||
{:profile-id profile-id
|
||||
:organization-id id
|
||||
:user-who-delete-member user-who-delete-member
|
||||
@ -369,33 +369,33 @@
|
||||
nil))
|
||||
|
||||
|
||||
(sv/defmethod ::leave-org
|
||||
(sv/defmethod ::leave-organization
|
||||
{::rpc/auth true
|
||||
::doc/added "2.15"
|
||||
::sm/params schema:leave-org
|
||||
::sm/params schema:leave-organization
|
||||
::db/transaction true}
|
||||
[cfg {:keys [::rpc/profile-id] :as params}]
|
||||
(leave-org cfg (assoc params
|
||||
:profile-id profile-id
|
||||
:user-who-delete-member profile-id
|
||||
:deleted-by-role "organization-member")))
|
||||
(leave-organization cfg (assoc params
|
||||
:profile-id profile-id
|
||||
:user-who-delete-member profile-id
|
||||
:deleted-by-role "organization-member")))
|
||||
|
||||
|
||||
(sv/defmethod ::get-leave-org-summary
|
||||
(sv/defmethod ::get-leave-organization-summary
|
||||
{::rpc/auth true
|
||||
::doc/added "2.18"
|
||||
::sm/params schema:get-leave-org-summary
|
||||
::sm/result schema:get-leave-org-summary-result
|
||||
::sm/params schema:get-leave-organization-summary
|
||||
::sm/result schema:get-leave-organization-summary-result
|
||||
::db/transaction true}
|
||||
[cfg {:keys [::rpc/profile-id id default-team-id]}]
|
||||
(let [{:keys [valid-teams-to-delete-ids
|
||||
valid-teams-to-transfer
|
||||
valid-teams-to-exit
|
||||
valid-default-team]} (get-valid-teams cfg id profile-id default-team-id)
|
||||
membership (nitrate/call cfg :get-org-membership
|
||||
membership (nitrate/call cfg :get-organization-membership
|
||||
{:profile-id profile-id
|
||||
:organization-id id})
|
||||
organization-members (nitrate/call cfg :get-org-members
|
||||
organization-members (nitrate/call cfg :get-organization-members
|
||||
{:organization-id id})
|
||||
teams-to-transfer-count (count valid-teams-to-transfer)
|
||||
teams-to-exit-count (count valid-teams-to-exit)]
|
||||
@ -403,20 +403,20 @@
|
||||
(ex/raise :type :validation
|
||||
:code :not-valid-teams))
|
||||
(assoc
|
||||
(get-leave-org-summary cfg default-team-id valid-teams-to-delete-ids teams-to-transfer-count teams-to-exit-count)
|
||||
(get-leave-organization-summary cfg default-team-id valid-teams-to-delete-ids teams-to-transfer-count teams-to-exit-count)
|
||||
:member-added-at (:created-at membership)
|
||||
:organization-member-count-before (count organization-members))))
|
||||
|
||||
|
||||
(def ^:private schema:remove-team-from-org
|
||||
(def ^:private schema:remove-team-from-organization
|
||||
[:map
|
||||
[:team-id ::sm/uuid]
|
||||
[:organization-id ::sm/uuid]
|
||||
[:organization-name ::sm/text]])
|
||||
|
||||
(sv/defmethod ::remove-team-from-org
|
||||
(sv/defmethod ::remove-team-from-organization
|
||||
{::doc/added "2.17"
|
||||
::sm/params schema:remove-team-from-org}
|
||||
::sm/params schema:remove-team-from-organization}
|
||||
[cfg {:keys [::rpc/profile-id team-id organization-id organization-name]}]
|
||||
|
||||
(assert-is-owner cfg profile-id team-id)
|
||||
@ -424,24 +424,24 @@
|
||||
(assert-membership cfg profile-id organization-id)
|
||||
;; Check moveTeams permission on the source organization
|
||||
(when (contains? cf/flags :nitrate)
|
||||
(let [org-perms (nitrate/call cfg :get-org-permissions
|
||||
{:organization-id organization-id})]
|
||||
(if (nil? org-perms)
|
||||
(let [organization-perms (nitrate/call cfg :get-organization-permissions
|
||||
{:organization-id organization-id})]
|
||||
(if (nil? organization-perms)
|
||||
(ex/raise :type :validation
|
||||
:code :not-allowed
|
||||
:hint "Unable to verify organization permissions")
|
||||
(when-not (nitrate-perms/allowed? :move-team
|
||||
{:org-perms org-perms
|
||||
{:organization-perms organization-perms
|
||||
:profile-id profile-id})
|
||||
(ex/raise :type :validation
|
||||
:code :not-allowed
|
||||
:hint "You are not allowed to move teams that are part of this organization. If you need more information, contact the owner.")))))
|
||||
|
||||
;; Api call to nitrate
|
||||
(nitrate/call cfg :remove-team-from-org {:team-id team-id :organization-id organization-id})
|
||||
(nitrate/call cfg :remove-team-from-organization {:team-id team-id :organization-id organization-id})
|
||||
|
||||
;; Notify connected users
|
||||
(notifications/notify-team-change cfg {:id team-id :organization {:name organization-name}} "dashboard.team-no-longer-belong-org")
|
||||
(notifications/notify-team-change cfg {:id team-id :organization {:name organization-name}} "dashboard.team-no-longer-belong-organization")
|
||||
nil)
|
||||
|
||||
(def ^:private sql:delete-team-external-invitations
|
||||
@ -457,12 +457,12 @@
|
||||
AND deleted_at IS NULL")
|
||||
|
||||
(defn- get-external-invitation-info
|
||||
"Returns info about external (non-org-member) invitations pending for a team.
|
||||
External invitations are those sent to users who are not members of the given org.
|
||||
"Returns info about external (non-organization-member) invitations pending for a team.
|
||||
External invitations are those sent to users who are not members of the given organization.
|
||||
Returns {:allows-anybody bool :external-emails [...]}"
|
||||
[{:keys [::db/conn] :as cfg} team-id organization-id]
|
||||
(let [org-perms (nitrate/call cfg :get-org-permissions {:organization-id organization-id})
|
||||
allows-anybody (nitrate-perms/allowed? :add-anybody-to-team {:org-perms org-perms})]
|
||||
(let [organization-perms (nitrate/call cfg :get-organization-permissions {:organization-id organization-id})
|
||||
allows-anybody (nitrate-perms/allowed? :add-anybody-to-team {:organization-perms organization-perms})]
|
||||
(if allows-anybody
|
||||
{:allows-anybody true :external-emails []}
|
||||
(let [emails (map :email (noh/get-team-invitation-emails conn team-id))]
|
||||
@ -470,9 +470,9 @@
|
||||
{:allows-anybody false :external-emails []}
|
||||
(let [emails-array (db/create-array conn "text" (vec emails))
|
||||
profiles (db/exec! conn [sql:get-profiles-by-emails emails-array])
|
||||
org-member-ids (into #{} (nitrate/call cfg :get-org-members {:organization-id organization-id}))
|
||||
organization-member-ids (into #{} (nitrate/call cfg :get-organization-members {:organization-id organization-id}))
|
||||
external-emails (->> profiles
|
||||
(remove #(contains? org-member-ids (:id %)))
|
||||
(remove #(contains? organization-member-ids (:id %)))
|
||||
(map :email)
|
||||
(vec))]
|
||||
{:allows-anybody false :external-emails external-emails}))))))
|
||||
@ -494,60 +494,60 @@
|
||||
(assert-membership cfg profile-id organization-id)
|
||||
|
||||
(when (contains? cf/flags :nitrate)
|
||||
(let [org-member-ids-before (into #{} (nitrate/call cfg :get-org-members {:organization-id organization-id}))
|
||||
team-with-org (nitrate/call cfg :get-team-org {:team-id team-id})
|
||||
source-org-id (get-in team-with-org [:organization :id])
|
||||
source-org-perms (when source-org-id
|
||||
(nitrate/call cfg :get-org-permissions
|
||||
{:organization-id source-org-id}))
|
||||
target-org-perms (nitrate/call cfg :get-org-permissions
|
||||
{:organization-id organization-id})
|
||||
target-org-same-owner? (and (some? source-org-perms)
|
||||
(some? target-org-perms)
|
||||
(= (:owner-id source-org-perms)
|
||||
(:owner-id target-org-perms)))]
|
||||
(when (nil? target-org-perms)
|
||||
(let [organization-member-ids-before (into #{} (nitrate/call cfg :get-organization-members {:organization-id organization-id}))
|
||||
team-with-organization (nitrate/call cfg :get-team-organization {:team-id team-id})
|
||||
source-organization-id (get-in team-with-organization [:organization :id])
|
||||
source-organization-perms (when source-organization-id
|
||||
(nitrate/call cfg :get-organization-permissions
|
||||
{:organization-id source-organization-id}))
|
||||
target-organization-perms (nitrate/call cfg :get-organization-permissions
|
||||
{:organization-id organization-id})
|
||||
target-organization-same-owner? (and (some? source-organization-perms)
|
||||
(some? target-organization-perms)
|
||||
(= (:owner-id source-organization-perms)
|
||||
(:owner-id target-organization-perms)))]
|
||||
(when (nil? target-organization-perms)
|
||||
(ex/raise :type :validation
|
||||
:code :not-allowed
|
||||
:hint "Unable to verify organization permissions"))
|
||||
|
||||
;; Team already belongs to an organization: check move-teams on source org.
|
||||
(when (some? source-org-id)
|
||||
(when (nil? source-org-perms)
|
||||
;; Team already belongs to an organization: check move-teams on the source organization.
|
||||
(when (some? source-organization-id)
|
||||
(when (nil? source-organization-perms)
|
||||
(ex/raise :type :validation
|
||||
:code :not-allowed
|
||||
:hint "Unable to verify organization permissions"))
|
||||
(when-not (nitrate-perms/allowed? :move-team
|
||||
{:org-perms source-org-perms
|
||||
{:organization-perms source-organization-perms
|
||||
:profile-id profile-id
|
||||
:target-org-same-owner? target-org-same-owner?})
|
||||
:target-organization-same-owner? target-organization-same-owner?})
|
||||
(ex/raise :type :validation
|
||||
:code :not-allowed
|
||||
:hint "You are not allowed to move teams that are part of this organization. If you need more information, contact the owner.")))
|
||||
|
||||
;; Always check target create-teams permission (new/add and move flows).
|
||||
(when-not (nitrate-perms/allowed? :create-team
|
||||
{:org-perms target-org-perms
|
||||
{:organization-perms target-organization-perms
|
||||
:profile-id profile-id})
|
||||
(ex/raise :type :validation
|
||||
:code :not-allowed
|
||||
:hint "You are not allowed to add teams in this organization"))
|
||||
|
||||
;; Add teammates to the org if needed
|
||||
;; Add teammates to the organization if needed
|
||||
(let [team-members (db/query cfg :team-profile-rel {:team-id team-id})
|
||||
new-member-ids (->> team-members
|
||||
(map :profile-id)
|
||||
(remove #{profile-id})
|
||||
(remove org-member-ids-before))]
|
||||
(remove organization-member-ids-before))]
|
||||
(doseq [member-id new-member-ids]
|
||||
(teams/initialize-user-in-nitrate-org cfg member-id organization-id)))
|
||||
(teams/initialize-user-in-nitrate-organization cfg member-id organization-id)))
|
||||
|
||||
;; Api call to nitrate
|
||||
(let [team (nitrate/call cfg :set-team-org {:team-id team-id
|
||||
:organization-id organization-id
|
||||
:is-default false})]
|
||||
(let [team (nitrate/call cfg :set-team-organization {:team-id team-id
|
||||
:organization-id organization-id
|
||||
:is-default false})]
|
||||
;; Notify connected users
|
||||
(notifications/notify-team-change cfg team "dashboard.team-belong-org"))
|
||||
(notifications/notify-team-change cfg team "dashboard.team-belong-organization"))
|
||||
|
||||
;; Delete pending invitations for users who are not members of the target organization
|
||||
(let [{:keys [allows-anybody external-emails]} (get-external-invitation-info cfg team-id organization-id)]
|
||||
@ -556,21 +556,21 @@
|
||||
emails-array (db/create-array conn "text" external-emails)]
|
||||
(db/exec! conn [sql:delete-team-external-invitations team-id emails-array]))))
|
||||
|
||||
;; Send warnings via email if the org has sso
|
||||
;; Send warnings via email if the organization has sso
|
||||
(neh/send-organization-setup-sso-emails-for-team!
|
||||
cfg organization-id team-id org-member-ids-before)))
|
||||
cfg organization-id team-id organization-member-ids-before)))
|
||||
|
||||
nil)
|
||||
|
||||
(def ^:private schema:check-org-members-params
|
||||
[:map {:title "CheckOrgMembersParams"}
|
||||
(def ^:private schema:check-organization-members-params
|
||||
[:map {:title "CheckOrganizationMembersParams"}
|
||||
[:organization-id ::sm/uuid]
|
||||
[:emails [:vector ::sm/email]]])
|
||||
|
||||
(sv/defmethod ::check-org-members
|
||||
(sv/defmethod ::check-organization-members
|
||||
{::rpc/auth true
|
||||
::doc/added "2.17"
|
||||
::sm/params schema:check-org-members-params
|
||||
::sm/params schema:check-organization-members-params
|
||||
::sm/result [:map-of :string :boolean]
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id organization-id emails]}]
|
||||
@ -579,23 +579,23 @@
|
||||
(let [emails-array (db/create-array conn "text" emails)
|
||||
profiles (db/exec! conn [sql:get-profiles-by-emails emails-array])
|
||||
email->id (into {} (map (fn [p] [(:email p) (:id p)])) profiles)
|
||||
org-member-ids (into #{} (nitrate/call cfg :get-org-members {:organization-id organization-id}))]
|
||||
organization-member-ids (into #{} (nitrate/call cfg :get-organization-members {:organization-id organization-id}))]
|
||||
(into {}
|
||||
(map (fn [email]
|
||||
(let [pid (get email->id email)]
|
||||
[email (boolean (and pid (contains? org-member-ids pid)))])))
|
||||
[email (boolean (and pid (contains? organization-member-ids pid)))])))
|
||||
emails)))
|
||||
{}))
|
||||
|
||||
(def ^:private schema:all-org-members-in-team-params
|
||||
[:map {:title "CheckOrgMembersInTeamParams"}
|
||||
(def ^:private schema:all-organization-members-in-team-params
|
||||
[:map {:title "CheckOrganizationMembersInTeamParams"}
|
||||
[:team-id ::sm/uuid]
|
||||
[:organization-id ::sm/uuid]])
|
||||
|
||||
(sv/defmethod ::all-org-members-in-team
|
||||
(sv/defmethod ::all-organization-members-in-team
|
||||
{::rpc/auth true
|
||||
::doc/added "2.17"
|
||||
::sm/params schema:all-org-members-in-team-params
|
||||
::sm/params schema:all-organization-members-in-team-params
|
||||
::sm/result ::sm/boolean}
|
||||
[cfg {:keys [::rpc/profile-id team-id organization-id]}]
|
||||
(if (contains? cf/flags :nitrate)
|
||||
@ -604,22 +604,22 @@
|
||||
(ex/raise :type :validation
|
||||
:code :insufficient-permissions))
|
||||
(assert-membership cfg profile-id organization-id)
|
||||
(let [org-members (nitrate/call cfg :get-org-members {:organization-id organization-id})
|
||||
org-member-ids (into #{} org-members)
|
||||
(let [organization-members (nitrate/call cfg :get-organization-members {:organization-id organization-id})
|
||||
organization-member-ids (into #{} organization-members)
|
||||
team-members (db/query cfg :team-profile-rel {:team-id team-id})
|
||||
team-member-ids (into #{} (map :profile-id team-members))]
|
||||
(every? #(contains? team-member-ids %) org-member-ids)))
|
||||
(every? #(contains? team-member-ids %) organization-member-ids)))
|
||||
false))
|
||||
|
||||
(def ^:private schema:all-team-members-in-orgs-params
|
||||
[:map {:title "CheckTeamMembersInOrgsParams"}
|
||||
(def ^:private schema:all-team-members-in-organizations-params
|
||||
[:map {:title "CheckTeamMembersInOrganizationsParams"}
|
||||
[:team-id ::sm/uuid]
|
||||
[:organization-ids [:vector ::sm/uuid]]])
|
||||
|
||||
(sv/defmethod ::all-team-members-in-orgs
|
||||
(sv/defmethod ::all-team-members-in-organizations
|
||||
{::rpc/auth true
|
||||
::doc/added "2.17"
|
||||
::sm/params schema:all-team-members-in-orgs-params
|
||||
::sm/params schema:all-team-members-in-organizations-params
|
||||
::sm/result [:map-of ::sm/uuid ::sm/boolean]}
|
||||
[cfg {:keys [::rpc/profile-id team-id organization-ids]}]
|
||||
(if (contains? cf/flags :nitrate)
|
||||
@ -630,15 +630,15 @@
|
||||
|
||||
(let [team-members (db/query cfg :team-profile-rel {:team-id team-id})
|
||||
team-member-ids (into #{} (map :profile-id team-members))]
|
||||
;; Validate requester membership in all orgs before fetching members.
|
||||
;; Validate requester membership in all organizations before fetching members.
|
||||
(run! #(assert-membership cfg profile-id %) organization-ids)
|
||||
|
||||
(into {}
|
||||
(map (fn [organization-id]
|
||||
(let [org-members (nitrate/call cfg :get-org-members {:organization-id organization-id})
|
||||
org-member-ids (into #{} org-members)]
|
||||
(let [organization-members (nitrate/call cfg :get-organization-members {:organization-id organization-id})
|
||||
organization-member-ids (into #{} organization-members)]
|
||||
[organization-id
|
||||
(every? #(contains? org-member-ids %) team-member-ids)])))
|
||||
(every? #(contains? organization-member-ids %) team-member-ids)])))
|
||||
organization-ids)))
|
||||
{}))
|
||||
|
||||
@ -682,7 +682,7 @@
|
||||
|
||||
(sv/defmethod ::check-nitrate-sso
|
||||
"Check if a user needs to login into the organization SSO.
|
||||
Accepts either team-id (to look up the org via the team) or organization-id directly.
|
||||
Accepts either team-id (to look up the organization via the team) or organization-id directly.
|
||||
Returns {:authorized true} when SSO is not active or the user cannot access the team.
|
||||
Returns {:authorized false :redirect-uri <url>} when SSO is active;
|
||||
the client must redirect there. The OIDC provider itself handles
|
||||
@ -702,11 +702,11 @@
|
||||
{:keys [authorized sso]} (nitrate/sso-session-authorized? cfg organization-id team-id request)]
|
||||
(if authorized
|
||||
{:authorized true}
|
||||
(if (oidc/org-sso-discovery-uri sso)
|
||||
(if (oidc/organization-sso-discovery-uri sso)
|
||||
{:authorized false
|
||||
:redirect-uri (oidc/build-org-sso-auth-redirect-uri cfg sso
|
||||
:dest-url url
|
||||
:organization-id organization-id)}
|
||||
:redirect-uri (oidc/build-organization-sso-auth-redirect-uri cfg sso
|
||||
:dest-url url
|
||||
:organization-id organization-id)}
|
||||
{:authorized false
|
||||
:redirect-uri nil}))))
|
||||
{:authorized true}))
|
||||
|
||||
@ -495,10 +495,10 @@
|
||||
{:id profile-id})
|
||||
|
||||
;; Delete owned organizations on the fly (no grace period).
|
||||
;; Nitrate iterates the user's owned orgs and, per org, calls
|
||||
;; Nitrate iterates the user's owned organizations and, per organization, calls
|
||||
;; Penpot back through two paths: ::notify-user-organizations-deletion
|
||||
;; (during delete-owned-orgs) and ::notify-organization-deletion.
|
||||
;; Both preserve org teams unchanged and only prefix or delete
|
||||
;; (during delete-owned-organizations) and ::notify-organization-deletion.
|
||||
;; Both preserve organization teams unchanged and only prefix or delete
|
||||
;; imported "Your Penpot" teams according to whether they still have files.
|
||||
;; Let Nitrate clean up the data associated with the deleted Penpot user:
|
||||
;; owned organizations, remaining memberships, and subscription cancellation.
|
||||
@ -562,7 +562,7 @@
|
||||
::sm/result schema:get-owned-organizations-summary-result}
|
||||
[cfg {:keys [::rpc/profile-id]}]
|
||||
(if (contains? cf/flags :nitrate)
|
||||
(or (nitrate/call cfg :get-owned-orgs-summary {:profile-id profile-id}) [])
|
||||
(or (nitrate/call cfg :get-owned-organizations-summary {:profile-id profile-id}) [])
|
||||
[]))
|
||||
|
||||
;; --- HELPERS
|
||||
|
||||
@ -198,7 +198,7 @@
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
(cond->> (get-teams conn profile-id)
|
||||
(contains? cf/flags :nitrate)
|
||||
(map #(nitrate/add-org-info-to-team cfg % params))
|
||||
(map #(nitrate/add-organization-info-to-team cfg % params))
|
||||
(contains? cf/flags :nitrate)
|
||||
(remove #(get-in % [:organization :expired-license])))))
|
||||
|
||||
@ -245,10 +245,10 @@
|
||||
[cfg {:keys [::rpc/profile-id id file-id] :as params}]
|
||||
(let [team (get-team cfg :profile-id profile-id :team-id id :file-id file-id)]
|
||||
(if (contains? cf/flags :nitrate)
|
||||
(nitrate/add-org-info-to-team cfg team params)
|
||||
(nitrate/add-organization-info-to-team cfg team params)
|
||||
team)))
|
||||
|
||||
(defn- get-org-owner-viewer-team
|
||||
(defn- get-organization-owner-viewer-team
|
||||
"When `profile-id` is a non-member owner of the organization that owns
|
||||
the requested team, returns the team shaped with viewer permissions;
|
||||
otherwise nil. `cfg` must carry the nitrate client."
|
||||
@ -305,7 +305,7 @@
|
||||
(-> result
|
||||
(decode-row)
|
||||
(process-permissions))
|
||||
(or (get-org-owner-viewer-team cfg profile-id default-team-id params)
|
||||
(or (get-organization-owner-viewer-team cfg profile-id default-team-id params)
|
||||
(ex/raise :type :not-found
|
||||
:code :team-does-not-exist)))))
|
||||
|
||||
@ -535,17 +535,17 @@
|
||||
(quotes/check! cfg {::quotes/id ::quotes/teams-per-profile
|
||||
::quotes/profile-id profile-id})
|
||||
|
||||
;; When creating inside an org, verify the user has permission to do so.
|
||||
;; Fail closed: if org permissions cannot be fetched, deny the operation.
|
||||
;; When creating inside an organization, verify the user has permission to do so.
|
||||
;; Fail closed: if organization permissions cannot be fetched, deny the operation.
|
||||
(when (and organization-id (contains? cf/flags :nitrate))
|
||||
(let [org-perms (nitrate/call cfg :get-org-permissions
|
||||
{:organization-id organization-id})]
|
||||
(if (nil? org-perms)
|
||||
(let [organization-perms (nitrate/call cfg :get-organization-permissions
|
||||
{:organization-id organization-id})]
|
||||
(if (nil? organization-perms)
|
||||
(ex/raise :type :validation
|
||||
:code :not-allowed
|
||||
:hint "Unable to verify organization permissions")
|
||||
(when-not (nitrate-perms/allowed? :create-team
|
||||
{:org-perms org-perms
|
||||
{:organization-perms organization-perms
|
||||
:profile-id profile-id})
|
||||
(ex/raise :type :validation
|
||||
:code :not-allowed
|
||||
@ -563,7 +563,7 @@
|
||||
{::audit/props {:id (:id team)}})))
|
||||
|
||||
|
||||
(defn create-default-org-team
|
||||
(defn create-default-organization-team
|
||||
[cfg profile-id organization-id]
|
||||
(quotes/check! cfg {::quotes/id ::quotes/teams-per-profile
|
||||
::quotes/profile-id profile-id})
|
||||
@ -579,11 +579,11 @@
|
||||
team (create-team cfg params)]
|
||||
(select-keys team [:id])))
|
||||
|
||||
(defn initialize-user-in-nitrate-org
|
||||
(defn initialize-user-in-nitrate-organization
|
||||
"If needed, create a default team for the user on the organization,
|
||||
and notify Nitrate that an user has been added to an org."
|
||||
and notify Nitrate that a user has been added to an organization."
|
||||
([cfg profile-id organization-id]
|
||||
(initialize-user-in-nitrate-org cfg profile-id organization-id nil))
|
||||
(initialize-user-in-nitrate-organization cfg profile-id organization-id nil))
|
||||
([cfg profile-id organization-id email]
|
||||
(assert (db/connection-map? cfg)
|
||||
"expected cfg with valid connection")
|
||||
@ -592,24 +592,24 @@
|
||||
cfg
|
||||
(fn [{:keys [::db/conn] :as tx-cfg}]
|
||||
|
||||
(let [membership (nitrate/call cfg :get-org-membership {:profile-id profile-id
|
||||
:organization-id organization-id})]
|
||||
(let [membership (nitrate/call cfg :get-organization-membership {:profile-id profile-id
|
||||
:organization-id organization-id})]
|
||||
;; Only when the user doesn't belong to the organization yet
|
||||
(when (and
|
||||
(some? (:organization-id membership)) ;; the organization exists
|
||||
(not (:is-member membership))) ;; the user is not a member of the org yet
|
||||
(not (:is-member membership))) ;; the user is not a member of the organization yet
|
||||
|
||||
|
||||
(let [organization-id organization-id
|
||||
default-team (create-default-org-team (assoc tx-cfg ::db/conn conn) profile-id organization-id)
|
||||
default-team (create-default-organization-team (assoc tx-cfg ::db/conn conn) profile-id organization-id)
|
||||
default-team-id (:id default-team)
|
||||
result (nitrate/call tx-cfg :add-profile-to-org (cond-> {:profile-id profile-id
|
||||
:team-id default-team-id
|
||||
:organization-id organization-id}
|
||||
(some? email) (assoc :email email)))]
|
||||
result (nitrate/call tx-cfg :add-profile-to-organization (cond-> {:profile-id profile-id
|
||||
:team-id default-team-id
|
||||
:organization-id organization-id}
|
||||
(some? email) (assoc :email email)))]
|
||||
(when (not (:is-member result))
|
||||
(ex/raise :type :internal
|
||||
:code :failed-add-profile-org-nitrate
|
||||
:code :failed-add-profile-organization-nitrate
|
||||
:context {:profile-id profile-id
|
||||
:organization-id organization-id
|
||||
:default-team-id default-team-id}))
|
||||
@ -622,12 +622,12 @@
|
||||
(assert (db/connection-map? cfg)
|
||||
"expected cfg with valid connection")
|
||||
(when (contains? cf/flags :nitrate)
|
||||
(let [membership (nitrate/call cfg :get-org-membership-by-team {:profile-id profile-id :team-id team-id})]
|
||||
(let [membership (nitrate/call cfg :get-organization-membership-by-team {:profile-id profile-id :team-id team-id})]
|
||||
;; Only when the team belong to an organization and the user is not a member
|
||||
(when (and
|
||||
(some? (:organization-id membership)) ;; the team do belong to an organization
|
||||
(not (:is-member membership))) ;; the user is not a member of the org yet
|
||||
(initialize-user-in-nitrate-org cfg profile-id (:organization-id membership)))))
|
||||
(not (:is-member membership))) ;; the user is not a member of the organization yet
|
||||
(initialize-user-in-nitrate-organization cfg profile-id (:organization-id membership)))))
|
||||
(db/insert! conn :team-profile-rel (assoc params :id (uuid/next)) options)))
|
||||
|
||||
(defn create-team
|
||||
@ -804,16 +804,16 @@
|
||||
|
||||
(let [team (get-team conn :profile-id profile-id :team-id team-id)
|
||||
team (if (contains? cf/flags :nitrate)
|
||||
(nitrate/add-org-info-to-team cfg team params)
|
||||
(nitrate/add-organization-info-to-team cfg team params)
|
||||
team)
|
||||
perms (get team :permissions)
|
||||
org (:organization team)
|
||||
in-org? (and (contains? cf/flags :nitrate) org)
|
||||
organization (:organization team)
|
||||
in-organization? (and (contains? cf/flags :nitrate) organization)
|
||||
can-delete?
|
||||
(if in-org?
|
||||
(if in-organization?
|
||||
(nitrate-perms/allowed? :delete-team
|
||||
{:org-perms {:owner-id (dm/get-in team [:organization :owner-id])
|
||||
:permissions (dm/get-in team [:organization :permissions])}
|
||||
{:organization-perms {:owner-id (dm/get-in team [:organization :owner-id])
|
||||
:permissions (dm/get-in team [:organization :permissions])}
|
||||
:profile-id profile-id
|
||||
:team-perms perms})
|
||||
(boolean (:is-owner perms)))]
|
||||
@ -823,8 +823,8 @@
|
||||
:code :only-owner-can-delete-team))
|
||||
|
||||
;; Protect the user's personal default team from deletion.
|
||||
;; Org-scoped default teams ("Your Penpot") are allowed to be deleted when they have no files.
|
||||
(when (and (:is-default team) (not in-org?))
|
||||
;; Organization-scoped default teams ("Your Penpot") are allowed to be deleted when they have no files.
|
||||
(when (and (:is-default team) (not in-organization?))
|
||||
(ex/raise :type :validation
|
||||
:code :non-deletable-team
|
||||
:hint "impossible to delete default team"))
|
||||
|
||||
@ -44,7 +44,7 @@
|
||||
update set role = ?, valid_until = ?, updated_at = now()
|
||||
returning *")
|
||||
|
||||
(def sql:upsert-org-invitation
|
||||
(def sql:upsert-organization-invitation
|
||||
"insert into team_invitation(id, team_id, org_id, email_to, created_by, role, valid_until)
|
||||
values (?, null, ?, ?, ?, ?, ?)
|
||||
on conflict(org_id, email_to) where team_id is null do
|
||||
@ -86,8 +86,8 @@
|
||||
[:role types.team/schema:role]
|
||||
[:email ::sm/email]])
|
||||
|
||||
(def ^:private schema:create-org-invitation
|
||||
[:map {:title "params:create-org-invitation"}
|
||||
(def ^:private schema:create-organization-invitation
|
||||
[:map {:title "params:create-organization-invitation"}
|
||||
[::rpc/profile-id ::sm/uuid]
|
||||
[:organization
|
||||
[:map
|
||||
@ -107,8 +107,8 @@
|
||||
(def ^:private check-create-invitation-params
|
||||
(sm/check-fn schema:create-invitation))
|
||||
|
||||
(def ^:private check-create-org-invitation-params
|
||||
(sm/check-fn schema:create-org-invitation))
|
||||
(def ^:private check-create-organization-invitation-params
|
||||
(sm/check-fn schema:create-organization-invitation))
|
||||
|
||||
(defn- allow-invitation-emails?
|
||||
[member]
|
||||
@ -116,24 +116,24 @@
|
||||
(not= :none (:email-invites notifications))))
|
||||
|
||||
(defn- assert-email-can-be-invited
|
||||
"Asserts that member is an org member when the org
|
||||
"Asserts that member is an organization member when the organization
|
||||
restricts who can be added to teams."
|
||||
[member org-member-ids]
|
||||
(when (some? org-member-ids)
|
||||
(let [is-member? (and (some? member) (contains? org-member-ids (:id member)))]
|
||||
[member organization-member-ids]
|
||||
(when (some? organization-member-ids)
|
||||
(let [is-member? (and (some? member) (contains? organization-member-ids (:id member)))]
|
||||
(when-not is-member?
|
||||
(ex/raise :type :validation
|
||||
:code :email-not-org-member
|
||||
:code :email-not-organization-member
|
||||
:hint "The invited email is not a member of the organization")))))
|
||||
|
||||
(defn- create-invitation
|
||||
[{:keys [::db/conn] :as cfg}
|
||||
{:keys [team organization profile role email org-member-ids organization-member-ids] :as params}]
|
||||
{:keys [team organization profile role email organization-member-ids all-organization-member-ids] :as params}]
|
||||
|
||||
(assert (db/connection-map? cfg)
|
||||
"expected cfg with valid connection")
|
||||
(if organization
|
||||
(assert (check-create-org-invitation-params params))
|
||||
(assert (check-create-organization-invitation-params params))
|
||||
(assert (check-create-invitation-params params)))
|
||||
|
||||
(let [email (profile/clean-email email)
|
||||
@ -145,11 +145,11 @@
|
||||
:code :email-domain-is-not-allowed
|
||||
:hint "email domain is in the blacklist"))
|
||||
|
||||
;; When nitrate is active and the team belongs to an org, check that
|
||||
;; the email is already an org member unless the org explicitly allows adding anybody.
|
||||
;; When nitrate is active and the team belongs to an organization, check that
|
||||
;; the email is already an organization member unless the organization explicitly allows adding anybody.
|
||||
(when (and (contains? cf/flags :nitrate)
|
||||
(:organization team))
|
||||
(assert-email-can-be-invited member org-member-ids))
|
||||
(assert-email-can-be-invited member organization-member-ids))
|
||||
|
||||
|
||||
;; When we have email verification disabled and invitation user is
|
||||
@ -165,9 +165,9 @@
|
||||
(get types.team/permissions-for-role role))]
|
||||
|
||||
(if organization
|
||||
;; Insert the invited member to the org
|
||||
;; Insert the invited member to the organization
|
||||
(when (contains? cf/flags :nitrate)
|
||||
(teams/initialize-user-in-nitrate-org cfg (:id member) (:id organization) email))
|
||||
(teams/initialize-user-in-nitrate-organization cfg (:id member) (:id organization) email))
|
||||
;; Insert the invited member to the team
|
||||
(teams/add-profile-to-team! cfg params {::db/on-conflict-do-nothing? true}))
|
||||
|
||||
@ -190,7 +190,7 @@
|
||||
(ct/in-future "876000h") ;; Organization invitations doesn't expire
|
||||
(ct/in-future "168h")) ;; 7 days
|
||||
invitation (db/exec-one! conn (if organization
|
||||
[sql:upsert-org-invitation id
|
||||
[sql:upsert-organization-invitation id
|
||||
(:id organization)
|
||||
(str/lower email)
|
||||
(:id profile)
|
||||
@ -233,7 +233,7 @@
|
||||
(boolean
|
||||
(and team-organization-id
|
||||
member
|
||||
(contains? organization-member-ids (:id member))))))
|
||||
(contains? all-organization-member-ids (:id member))))))
|
||||
itoken (create-invitation-token cfg tprops)
|
||||
ptoken (create-profile-identity-token cfg profile-id)]
|
||||
|
||||
@ -255,7 +255,7 @@
|
||||
(if organization
|
||||
(when (contains? cf/flags :nitrate)
|
||||
(eml/send! {::eml/conn conn
|
||||
::eml/factory eml/invite-to-org
|
||||
::eml/factory eml/invite-to-organization
|
||||
:public-uri (cf/get :public-uri)
|
||||
:to email
|
||||
:invited-by (:fullname profile)
|
||||
@ -275,7 +275,7 @@
|
||||
|
||||
itoken)))))
|
||||
|
||||
(defn create-org-invitation
|
||||
(defn create-organization-invitation
|
||||
[cfg {:keys [::rpc/profile-id] :as params}]
|
||||
(let [profile (db/get-by-id cfg :profile profile-id)]
|
||||
(create-invitation cfg
|
||||
@ -345,21 +345,21 @@
|
||||
- emails (set) + role (single role for all emails)
|
||||
- invitations (vector of {:email :role} maps)"
|
||||
[{:keys [::db/conn] :as cfg} {:keys [profile team role emails invitations] :as params}]
|
||||
(let [;; Enrich team with org info once for all invitations when nitrate is active
|
||||
(let [;; Enrich team with organization info once for all invitations when nitrate is active
|
||||
team (if (contains? cf/flags :nitrate)
|
||||
(nitrate/add-org-info-to-team cfg team {})
|
||||
(nitrate/add-organization-info-to-team cfg team {})
|
||||
team)
|
||||
org (:organization team)
|
||||
org-id (:id org)
|
||||
restricted? (and org-id (not (nitrate-perms/allowed? :add-anybody-to-team {:org-perms org})))
|
||||
organization-member-ids
|
||||
(when org-id
|
||||
(into #{} (nitrate/call cfg :get-org-members {:organization-id org-id})))
|
||||
org-member-ids (when restricted? organization-member-ids)
|
||||
organization (:organization team)
|
||||
organization-id (:id organization)
|
||||
restricted? (and organization-id (not (nitrate-perms/allowed? :add-anybody-to-team {:organization-perms organization})))
|
||||
all-organization-member-ids
|
||||
(when organization-id
|
||||
(into #{} (nitrate/call cfg :get-organization-members {:organization-id organization-id})))
|
||||
organization-member-ids (when restricted? all-organization-member-ids)
|
||||
params (assoc params
|
||||
:team team
|
||||
:org-member-ids org-member-ids
|
||||
:organization-member-ids organization-member-ids)
|
||||
:organization-member-ids organization-member-ids
|
||||
:all-organization-member-ids all-organization-member-ids)
|
||||
|
||||
;; Normalize input to a consistent format: [{:email :role}]
|
||||
invitation-data (cond
|
||||
|
||||
@ -101,6 +101,17 @@
|
||||
|
||||
;; --- Team Invitation
|
||||
|
||||
(def ^:private sql:get-organization-invitation
|
||||
"SELECT *
|
||||
FROM team_invitation
|
||||
WHERE email_to = ?
|
||||
AND org_id = ?")
|
||||
|
||||
(def ^:private sql:delete-organization-invitation
|
||||
"DELETE FROM team_invitation
|
||||
WHERE email_to = ?
|
||||
AND org_id = ?")
|
||||
|
||||
(defn- accept-invitation
|
||||
[{:keys [::db/conn] :as cfg}
|
||||
{:keys [team-id organization-id role member-email] :as claims} invitation member]
|
||||
@ -124,9 +135,9 @@
|
||||
(get types.team/permissions-for-role role))
|
||||
|
||||
accepted-team-id (if organization-id
|
||||
;; Insert the invited member to the org
|
||||
;; Insert the invited member to the organization
|
||||
(when (contains? cf/flags :nitrate)
|
||||
(teams/initialize-user-in-nitrate-org cfg id-member organization-id member-email))
|
||||
(teams/initialize-user-in-nitrate-organization cfg id-member organization-id member-email))
|
||||
;; Insert the invited member to the team
|
||||
(do (teams/add-profile-to-team! cfg params {::db/on-conflict-do-nothing? true})
|
||||
team-id))]
|
||||
@ -145,10 +156,11 @@
|
||||
{:id id-member}))
|
||||
|
||||
;; Delete the invitation
|
||||
(db/delete! conn :team-invitation
|
||||
(cond-> {:email-to member-email}
|
||||
team-id (assoc :team-id team-id)
|
||||
organization-id (assoc :org-id organization-id)))
|
||||
(if organization-id
|
||||
(db/exec-one! conn [sql:delete-organization-invitation member-email organization-id])
|
||||
(db/delete! conn :team-invitation
|
||||
{:email-to member-email
|
||||
:team-id team-id}))
|
||||
|
||||
;; Delete any request (only applicable for team invitations)
|
||||
(when team-id
|
||||
@ -184,16 +196,17 @@
|
||||
:code :invalid-invitation-token
|
||||
:hint "invitation token contains unexpected data"))
|
||||
|
||||
(let [invitation (db/get* conn :team-invitation
|
||||
(cond-> {:email-to member-email}
|
||||
team-id (assoc :team-id team-id)
|
||||
organization-id (assoc :org-id organization-id)))
|
||||
(let [invitation (if organization-id
|
||||
(db/exec-one! conn [sql:get-organization-invitation member-email organization-id])
|
||||
(db/get* conn :team-invitation
|
||||
{:email-to member-email
|
||||
:team-id team-id}))
|
||||
profile (db/get* conn :profile
|
||||
{:id profile-id}
|
||||
{:columns [:id :email :default-team-id]})
|
||||
registration-disabled? (not (contains? cf/flags :registration))
|
||||
|
||||
org-invitation? (and (contains? cf/flags :nitrate) organization-id)]
|
||||
organization-invitation? (and (contains? cf/flags :nitrate) organization-id)]
|
||||
|
||||
(if profile
|
||||
(do
|
||||
@ -219,12 +232,12 @@
|
||||
(when (contains? cf/flags :nitrate)
|
||||
(cond
|
||||
organization-id
|
||||
(nitrate/call cfg :get-org-membership {:profile-id profile-id
|
||||
:organization-id organization-id})
|
||||
(nitrate/call cfg :get-organization-membership {:profile-id profile-id
|
||||
:organization-id organization-id})
|
||||
|
||||
team-id
|
||||
(nitrate/call cfg :get-org-membership-by-team {:profile-id profile-id
|
||||
:team-id team-id})))
|
||||
(nitrate/call cfg :get-organization-membership-by-team {:profile-id profile-id
|
||||
:team-id team-id})))
|
||||
|
||||
organization-id-on-add
|
||||
(when (and (:organization-id membership)
|
||||
@ -246,19 +259,19 @@
|
||||
organization-member-count-before
|
||||
(when organization-id-on-add
|
||||
(count
|
||||
(nitrate/call cfg :get-org-members
|
||||
(nitrate/call cfg :get-organization-members
|
||||
{:organization-id organization-id-on-add})))]
|
||||
|
||||
(when (:is-member membership)
|
||||
(when org-invitation?
|
||||
(when organization-invitation?
|
||||
(ex/raise :type :validation
|
||||
:code :already-an-org-member
|
||||
:code :already-an-organization-member
|
||||
:team-id (:default-team-id membership)
|
||||
:hint "the user is already a member of the organization")))
|
||||
|
||||
(when (and org-invitation? (not (:organization-id membership)))
|
||||
(when (and organization-invitation? (not (:organization-id membership)))
|
||||
(ex/raise :type :validation
|
||||
:code :org-not-found
|
||||
:code :organization-not-found
|
||||
:team-id (:default-team-id profile)
|
||||
:hint "the organization doesn't exist"))
|
||||
|
||||
@ -299,10 +312,10 @@
|
||||
(audit/clean-props))))))
|
||||
|
||||
(cond-> (assoc claims :state :created)
|
||||
;; when the invitation is to an org, instead of a team, add the
|
||||
;; accepted-team-id as :org-team-id
|
||||
;; when the invitation is to an organization, instead of a team, add the
|
||||
;; accepted-team-id as :organization-team-id
|
||||
(:organization-id claims)
|
||||
(assoc :org-team-id accepted-team-id)
|
||||
(assoc :organization-team-id accepted-team-id)
|
||||
|
||||
organization-id-on-add
|
||||
(assoc :organization-invitation-audit
|
||||
@ -320,7 +333,7 @@
|
||||
;; If the user is not logged-in and the invitation has been canceled
|
||||
;; we return a specific error code so the frontend can redirect to
|
||||
;; login with an appropriate message instead of showing the error page.
|
||||
;; This only applies to org invitations; team invitations keep the
|
||||
;; This only applies to organization invitations; team invitations keep the
|
||||
;; existing :invalid-token behavior.
|
||||
(when (nil? invitation)
|
||||
(ex/raise :type :validation
|
||||
|
||||
@ -167,12 +167,12 @@
|
||||
[:role ::sm/text]])
|
||||
|
||||
(sv/defmethod ::notify-user-added-to-organization
|
||||
"Notify to Penpot that an user has joined an org from nitrate"
|
||||
"Notify to Penpot that an user has joined an organization from nitrate"
|
||||
{::doc/added "2.14"
|
||||
::sm/params schema:notify-user-added-to-organization
|
||||
::rpc/auth false}
|
||||
[cfg {:keys [profile-id organization-id]}]
|
||||
(db/tx-run! cfg teams/create-default-org-team profile-id organization-id))
|
||||
(db/tx-run! cfg teams/create-default-organization-team profile-id organization-id))
|
||||
|
||||
|
||||
;; ---- API: get-managed-profiles
|
||||
@ -314,7 +314,7 @@ RETURNING id, deleted_at;")
|
||||
nil)
|
||||
|
||||
(defn manage-deleted-organization-teams
|
||||
"For a deleted organization, preserve org teams unchanged and only prefix or
|
||||
"For a deleted organization, preserve organization teams unchanged and only prefix or
|
||||
delete member Your Penpot teams depending on whether they still contain files."
|
||||
[cfg {:keys [organization-id organization-name teams]}]
|
||||
(let [all-team-ids (->> teams
|
||||
@ -329,7 +329,7 @@ RETURNING id, deleted_at;")
|
||||
distinct
|
||||
(into []))]
|
||||
(when (seq all-team-ids)
|
||||
(let [org-prefix (str "[" (d/sanitize-string organization-name) "] ")]
|
||||
(let [organization-prefix (str "[" (d/sanitize-string organization-name) "] ")]
|
||||
(db/tx-run!
|
||||
cfg
|
||||
(fn [{:keys [::db/conn] :as cfg}]
|
||||
@ -343,11 +343,11 @@ RETURNING id, deleted_at;")
|
||||
teams-to-prefix (->> your-penpot-team-ids (filter teams-with-files) (into []))
|
||||
teams-to-delete (->> your-penpot-team-ids (remove teams-with-files) (into []))]
|
||||
|
||||
;; Org teams move to the fallback org unchanged. Only imported
|
||||
;; Your Penpot teams keep the org prefix when they still have files.
|
||||
;; Organization teams move to the fallback organization unchanged. Only imported
|
||||
;; Your Penpot teams keep the organization prefix when they still have files.
|
||||
(when (seq teams-to-prefix)
|
||||
(db/exec! conn [sql:prefix-teams-name-and-unset-default
|
||||
org-prefix
|
||||
organization-prefix
|
||||
(db/create-array conn "uuid" teams-to-prefix)]))
|
||||
|
||||
;; Empty imported Your Penpot teams disappear entirely.
|
||||
@ -358,16 +358,16 @@ RETURNING id, deleted_at;")
|
||||
|
||||
|
||||
(sv/defmethod ::notify-organization-deletion
|
||||
"For a deleted organization, preserve org teams and only prefix or delete
|
||||
"For a deleted organization, preserve organization teams and only prefix or delete
|
||||
imported Your Penpot teams before notifying connected users."
|
||||
{::doc/added "2.15"
|
||||
::sm/params schema:notify-organization-deletion
|
||||
::rpc/auth false}
|
||||
[cfg {:keys [organization-id]}]
|
||||
(let [org-summary (nitrate/call cfg :get-org-summary {:organization-id organization-id})
|
||||
teams (:teams org-summary)]
|
||||
(manage-deleted-organization-teams cfg {:organization-name (:name org-summary)
|
||||
:organization-id (:id org-summary)
|
||||
(let [organization-summary (nitrate/call cfg :get-organization-summary {:organization-id organization-id})
|
||||
teams (:teams organization-summary)]
|
||||
(manage-deleted-organization-teams cfg {:organization-name (:name organization-summary)
|
||||
:organization-id (:id organization-summary)
|
||||
:teams teams})
|
||||
nil))
|
||||
|
||||
@ -378,18 +378,18 @@ RETURNING id, deleted_at;")
|
||||
[:profile-id ::sm/uuid]])
|
||||
|
||||
(sv/defmethod ::notify-user-organizations-deletion
|
||||
"For a given user, find all owned organizations and apply the deleted-org
|
||||
"For a given user, find all owned organizations and apply the deleted-organization
|
||||
transfer rules to their imported Your Penpot teams."
|
||||
{::doc/added "2.18"
|
||||
::sm/params schema:notify-user-organizations-deletion
|
||||
::nitrate/sso false}
|
||||
[cfg {:keys [profile-id]}]
|
||||
(let [owned-orgs (nitrate/call cfg :get-owned-orgs {:profile-id profile-id})]
|
||||
(doseq [org owned-orgs]
|
||||
(let [organization-name (:name org)
|
||||
teams (:teams org)]
|
||||
(let [owned-organizations (nitrate/call cfg :get-owned-organizations {:profile-id profile-id})]
|
||||
(doseq [organization owned-organizations]
|
||||
(let [organization-name (:name organization)
|
||||
teams (:teams organization)]
|
||||
(manage-deleted-organization-teams cfg {:organization-name organization-name
|
||||
:organization-id (:id org)
|
||||
:organization-id (:id organization)
|
||||
:teams teams}))))
|
||||
nil)
|
||||
|
||||
@ -496,7 +496,7 @@ RETURNING id, deleted_at;")
|
||||
[:organization schema:organization-with-avatar]]
|
||||
::nitrate/sso false}
|
||||
[cfg params]
|
||||
(db/tx-run! cfg ti/create-org-invitation params)
|
||||
(db/tx-run! cfg ti/create-organization-invitation params)
|
||||
nil)
|
||||
|
||||
|
||||
@ -524,9 +524,9 @@ RETURNING id, deleted_at;")
|
||||
::sm/result schema:get-organization-invitations-result
|
||||
::nitrate/sso false}
|
||||
[cfg {:keys [organization-id]}]
|
||||
(let [team-ids (noh/get-org-team-ids cfg organization-id)]
|
||||
(let [team-ids (noh/get-organization-team-ids cfg organization-id)]
|
||||
(db/run! cfg (fn [{:keys [::db/conn]}]
|
||||
(->> (noh/get-org-invitations conn organization-id team-ids)
|
||||
(->> (noh/get-organization-invitations conn organization-id team-ids)
|
||||
(mapv (fn [{:keys [photo-id] :as invitation}]
|
||||
(cond-> (dissoc invitation :photo-id)
|
||||
photo-id
|
||||
@ -546,13 +546,13 @@ RETURNING id, deleted_at;")
|
||||
[:email ::sm/email]])
|
||||
|
||||
(sv/defmethod ::delete-organization-invitations
|
||||
"Delete all invitations for one email in an organization scope (org + org teams)."
|
||||
"Delete all invitations for one email in an organization scope (organization + organization teams)."
|
||||
{::doc/added "2.16"
|
||||
::sm/params schema:delete-organization-invitations-params
|
||||
::nitrate/sso false}
|
||||
[cfg {:keys [organization-id email]}]
|
||||
(let [clean-email (profile/clean-email email)
|
||||
team-ids (noh/get-org-team-ids cfg organization-id)]
|
||||
team-ids (noh/get-organization-team-ids cfg organization-id)]
|
||||
(db/run! cfg (fn [{:keys [::db/conn]}]
|
||||
(let [ids-array (db/create-array conn "uuid" team-ids)]
|
||||
(db/exec! conn [sql:delete-organization-invitations clean-email organization-id ids-array]))))
|
||||
@ -571,14 +571,14 @@ RETURNING id, deleted_at;")
|
||||
[:organization-id ::sm/uuid]])
|
||||
|
||||
(sv/defmethod ::delete-all-organization-invitations
|
||||
"Delete every pending invitation associated with an organization (org-level + team-level).
|
||||
"Delete every pending invitation associated with an organization (organization-level + team-level).
|
||||
Called from Nitrate when an organization is about to be deleted, so users that click
|
||||
their invitation token hit the existing invalid-token landing page."
|
||||
{::doc/added "2.18"
|
||||
::sm/params schema:delete-all-organization-invitations-params
|
||||
::rpc/auth false}
|
||||
[cfg {:keys [organization-id]}]
|
||||
(let [team-ids (noh/get-org-team-ids cfg organization-id)]
|
||||
(let [team-ids (noh/get-organization-team-ids cfg organization-id)]
|
||||
(db/run! cfg (fn [{:keys [::db/conn]}]
|
||||
(let [ids-array (db/create-array conn "uuid" team-ids)]
|
||||
(db/exec! conn [sql:delete-all-organization-invitations organization-id ids-array]))))
|
||||
@ -633,16 +633,16 @@ RETURNING id, deleted_at;")
|
||||
valid-teams-to-leave (into valid-teams-to-exit
|
||||
(map add-reassign-to valid-teams-to-transfer))]
|
||||
|
||||
(cnit/leave-org cfg (assoc params
|
||||
:id organization-id
|
||||
:name organization-name
|
||||
:teams-to-delete valid-teams-to-delete-ids
|
||||
:teams-to-leave valid-teams-to-leave
|
||||
:skip-validation true
|
||||
:user-who-delete-member actor-profile-id
|
||||
:deleted-by-role (when actor-profile-id
|
||||
"organization-owner")))
|
||||
(notifications/notify-user-org-change cfg profile-id organization-id organization-name "dashboard.user-no-longer-belong-org")
|
||||
(cnit/leave-organization cfg (assoc params
|
||||
:id organization-id
|
||||
:name organization-name
|
||||
:teams-to-delete valid-teams-to-delete-ids
|
||||
:teams-to-leave valid-teams-to-leave
|
||||
:skip-validation true
|
||||
:user-who-delete-member actor-profile-id
|
||||
:deleted-by-role (when actor-profile-id
|
||||
"organization-owner")))
|
||||
(notifications/notify-user-organization-change cfg profile-id organization-id organization-name "dashboard.user-no-longer-belong-organization")
|
||||
nil))
|
||||
|
||||
;; API: get-remove-from-organization-summary
|
||||
@ -673,11 +673,11 @@ RETURNING id, deleted_at;")
|
||||
(when-not valid-default-team
|
||||
(ex/raise :type :validation
|
||||
:code :not-valid-teams))
|
||||
(cnit/get-leave-org-summary cfg
|
||||
default-team-id
|
||||
valid-teams-to-delete-ids
|
||||
(count valid-teams-to-transfer)
|
||||
(count valid-teams-to-exit))))
|
||||
(cnit/get-leave-organization-summary cfg
|
||||
default-team-id
|
||||
valid-teams-to-delete-ids
|
||||
(count valid-teams-to-transfer)
|
||||
(count valid-teams-to-exit))))
|
||||
|
||||
;; API: send-renewal-email
|
||||
|
||||
@ -720,7 +720,7 @@ RETURNING id, deleted_at;")
|
||||
WHERE id = ANY(?)
|
||||
AND deleted_at IS NULL")
|
||||
|
||||
(def ^:private sql:exists-non-member-org-team-invitations
|
||||
(def ^:private sql:exists-non-member-organization-team-invitations
|
||||
"SELECT EXISTS (
|
||||
SELECT 1
|
||||
FROM team_invitation
|
||||
@ -728,13 +728,13 @@ RETURNING id, deleted_at;")
|
||||
AND email_to <> ALL(?)
|
||||
) AS non_member")
|
||||
|
||||
(def ^:private sql:delete-non-member-org-team-invitations
|
||||
(def ^:private sql:delete-non-member-organization-team-invitations
|
||||
"DELETE FROM team_invitation
|
||||
WHERE team_id = ANY(?)
|
||||
AND email_to <> ALL(?)
|
||||
RETURNING email_to")
|
||||
|
||||
(def ^:private schema:org-team-invitations-for-non-members-params
|
||||
(def ^:private schema:organization-team-invitations-for-non-members-params
|
||||
[:map
|
||||
[:team-ids [:vector ::sm/uuid]]
|
||||
[:member-ids [:vector ::sm/uuid]]])
|
||||
@ -742,8 +742,8 @@ RETURNING id, deleted_at;")
|
||||
(def ^:private schema:exists-organization-team-invitations-for-non-members-result
|
||||
[:map [:exists ::sm/boolean]])
|
||||
|
||||
(defn- org-team-invitations-for-non-members-arrays
|
||||
"Member emails and PG arrays used by exists/delete org team invitation endpoints."
|
||||
(defn- organization-team-invitations-for-non-members-arrays
|
||||
"Member emails and PG arrays used by exists/delete organization team invitation endpoints."
|
||||
[conn {:keys [team-ids member-ids]}]
|
||||
(let [member-ids-array (db/create-array conn "uuid" member-ids)
|
||||
member-emails (->> (db/exec! conn [sql:get-profile-emails-by-ids member-ids-array])
|
||||
@ -752,11 +752,11 @@ RETURNING id, deleted_at;")
|
||||
{:emails-array (db/create-array conn "text" (vec member-emails))
|
||||
:teams-array (db/create-array conn "uuid" team-ids)}))
|
||||
|
||||
(defn- non-member-org-team-invitations-exist?
|
||||
(defn- non-member-organization-team-invitations-exist?
|
||||
[conn params]
|
||||
(let [{:keys [emails-array teams-array]}
|
||||
(org-team-invitations-for-non-members-arrays conn params)]
|
||||
(-> (db/exec-one! conn [sql:exists-non-member-org-team-invitations
|
||||
(organization-team-invitations-for-non-members-arrays conn params)]
|
||||
(-> (db/exec-one! conn [sql:exists-non-member-organization-team-invitations
|
||||
teams-array
|
||||
emails-array])
|
||||
:non-member)))
|
||||
@ -764,24 +764,24 @@ RETURNING id, deleted_at;")
|
||||
(sv/defmethod ::exists-organization-team-invitations-for-non-members
|
||||
"Return if there are any team invitations for emails that are not organization members."
|
||||
{::doc/added "2.18"
|
||||
::sm/params schema:org-team-invitations-for-non-members-params
|
||||
::sm/params schema:organization-team-invitations-for-non-members-params
|
||||
::sm/result schema:exists-organization-team-invitations-for-non-members-result
|
||||
::nitrate/sso false}
|
||||
[cfg params]
|
||||
(db/run! cfg (fn [{:keys [::db/conn]}]
|
||||
{:exists (boolean (non-member-org-team-invitations-exist? conn params))})))
|
||||
{:exists (boolean (non-member-organization-team-invitations-exist? conn params))})))
|
||||
|
||||
(sv/defmethod ::delete-organization-team-invitations-for-non-members
|
||||
"Delete team invitations for emails that are not organization members."
|
||||
{::doc/added "2.18"
|
||||
::sm/params schema:org-team-invitations-for-non-members-params
|
||||
::sm/params schema:organization-team-invitations-for-non-members-params
|
||||
::db/transaction true
|
||||
::nitrate/sso false}
|
||||
[cfg params]
|
||||
(db/run! cfg (fn [{:keys [::db/conn]}]
|
||||
(let [{:keys [emails-array teams-array]}
|
||||
(org-team-invitations-for-non-members-arrays conn params)]
|
||||
(db/exec! conn [sql:delete-non-member-org-team-invitations
|
||||
(organization-team-invitations-for-non-members-arrays conn params)]
|
||||
(db/exec! conn [sql:delete-non-member-organization-team-invitations
|
||||
teams-array
|
||||
emails-array])
|
||||
nil))))
|
||||
@ -917,8 +917,8 @@ RETURNING id, deleted_at;")
|
||||
::sm/result schema:get-teams-detail-result
|
||||
::nitrate/sso false}
|
||||
[cfg {:keys [organization-id]}]
|
||||
(let [org-summary (nitrate/call cfg :get-org-summary {:organization-id organization-id})
|
||||
team-ids (into [] (comp d/xf:map-id (filter uuid?)) (:teams org-summary))]
|
||||
(let [organization-summary (nitrate/call cfg :get-organization-summary {:organization-id organization-id})
|
||||
team-ids (into [] (comp d/xf:map-id (filter uuid?)) (:teams organization-summary))]
|
||||
(if (empty? team-ids)
|
||||
[]
|
||||
(db/run! cfg
|
||||
@ -958,8 +958,8 @@ RETURNING id, deleted_at;")
|
||||
::rpc/auth false}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [organization-id updated-props announce-activation]}]
|
||||
(when updated-props
|
||||
(rpc/invalidate-org-sso-cache-by-org! organization-id)
|
||||
(session/clear-org-sso-sessions! pool organization-id))
|
||||
(rpc/invalidate-organization-sso-cache-by-organization! organization-id)
|
||||
(session/clear-organization-sso-sessions! pool organization-id))
|
||||
(notifications/notify-organization-change-sso cfg organization-id)
|
||||
(when announce-activation
|
||||
(neh/send-organization-setup-sso-emails! cfg organization-id))
|
||||
|
||||
@ -28,11 +28,11 @@
|
||||
WHERE email = ANY(?)
|
||||
AND deleted_at IS NULL")
|
||||
|
||||
(defn- org-sso-active?
|
||||
(defn- organization-sso-active?
|
||||
"Return whether SSO is enabled for the organization."
|
||||
[cfg organization-id]
|
||||
(when (contains? cf/flags :nitrate)
|
||||
(true? (:active (nitrate/call cfg :get-org-sso {:organization-id organization-id})))))
|
||||
(true? (:active (nitrate/call cfg :get-organization-sso {:organization-id organization-id})))))
|
||||
|
||||
(def ^:private xf:map-email (map :email))
|
||||
|
||||
@ -61,44 +61,44 @@
|
||||
:to email
|
||||
:organization-name organization-name})))
|
||||
|
||||
(defn- get-org-sso-notify-recipients
|
||||
"Unique org members and pending org/team invitees for SSO activation emails."
|
||||
[conn cfg organization-id org-summary]
|
||||
(let [member-ids (nitrate/call cfg :get-org-members {:organization-id organization-id})
|
||||
team-ids (neh/get-org-team-ids org-summary)
|
||||
(defn- get-organization-sso-notify-recipients
|
||||
"Unique organization members and pending organization/team invitees for SSO activation emails."
|
||||
[conn cfg organization-id organization-summary]
|
||||
(let [member-ids (nitrate/call cfg :get-organization-members {:organization-id organization-id})
|
||||
team-ids (neh/get-organization-team-ids organization-summary)
|
||||
member-emails (if (seq member-ids)
|
||||
(let [ids-array (db/create-array conn "uuid" member-ids)]
|
||||
(into #{} (map :email (db/exec! conn [sql:get-profile-emails-by-ids ids-array]))))
|
||||
#{})
|
||||
invite-emails (into #{} (map :email
|
||||
(neh/get-org-invitations conn organization-id team-ids)))
|
||||
(neh/get-organization-invitations conn organization-id team-ids)))
|
||||
emails (into #{} (concat member-emails invite-emails))]
|
||||
(recipients-by-emails conn emails)))
|
||||
|
||||
(defn- get-team-sso-notify-recipients
|
||||
"Team members who are not in `org-member-ids`, plus pending team invitations."
|
||||
[conn team-id org-member-ids]
|
||||
"Team members who are not in `organization-member-ids`, plus pending team invitations."
|
||||
[conn team-id organization-member-ids]
|
||||
(let [team-members (->> (teams/get-team-members conn team-id)
|
||||
(remove #(contains? org-member-ids (:id %))))
|
||||
(remove #(contains? organization-member-ids (:id %))))
|
||||
invitations (neh/get-team-invitation-emails conn team-id)]
|
||||
(->> (sequence xf:map-email (concat team-members invitations))
|
||||
(recipients-by-emails conn))))
|
||||
|
||||
(defn send-organization-setup-sso-emails!
|
||||
"Notify all org members and pending org/team invitees that SSO is active."
|
||||
"Notify all organization members and pending organization/team invitees that SSO is active."
|
||||
[cfg organization-id]
|
||||
(let [org-summary (nitrate/call cfg :get-org-summary {:organization-id organization-id})]
|
||||
(let [organization-summary (nitrate/call cfg :get-organization-summary {:organization-id organization-id})]
|
||||
(db/tx-run! cfg
|
||||
(fn [{:keys [::db/conn]}]
|
||||
(doseq [recipient (get-org-sso-notify-recipients conn cfg organization-id org-summary)]
|
||||
(send-organization-setup-sso-email! conn (:name org-summary) recipient))))))
|
||||
(doseq [recipient (get-organization-sso-notify-recipients conn cfg organization-id organization-summary)]
|
||||
(send-organization-setup-sso-email! conn (:name organization-summary) recipient))))))
|
||||
|
||||
(defn send-organization-setup-sso-emails-for-team!
|
||||
"Notify team members who are not in `org-member-ids-before` and pending team invitees."
|
||||
[cfg organization-id team-id org-member-ids-before]
|
||||
(when (org-sso-active? cfg organization-id)
|
||||
(let [org-summary (nitrate/call cfg :get-org-summary {:organization-id organization-id})]
|
||||
"Notify team members who are not in `organization-member-ids-before` and pending team invitees."
|
||||
[cfg organization-id team-id organization-member-ids-before]
|
||||
(when (organization-sso-active? cfg organization-id)
|
||||
(let [organization-summary (nitrate/call cfg :get-organization-summary {:organization-id organization-id})]
|
||||
(db/tx-run! cfg
|
||||
(fn [{:keys [::db/conn]}]
|
||||
(doseq [recipient (get-team-sso-notify-recipients conn team-id org-member-ids-before)]
|
||||
(send-organization-setup-sso-email! conn (:name org-summary) recipient)))))))
|
||||
(doseq [recipient (get-team-sso-notify-recipients conn team-id organization-member-ids-before)]
|
||||
(send-organization-setup-sso-email! conn (:name organization-summary) recipient)))))))
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
[app.db :as db]
|
||||
[app.nitrate :as nitrate]))
|
||||
|
||||
(def ^:private sql:get-org-invitations
|
||||
(def ^:private sql:get-organization-invitations
|
||||
"SELECT DISTINCT ON (email_to)
|
||||
ti.id,
|
||||
ti.org_id AS organization_id,
|
||||
@ -35,24 +35,24 @@ LEFT JOIN profile AS p
|
||||
AND ti.valid_until >= now()
|
||||
ORDER BY ti.email_to, ti.valid_until DESC, ti.created_at DESC;")
|
||||
|
||||
(defn get-org-team-ids
|
||||
(defn get-organization-team-ids
|
||||
"Return team ids for an organization.
|
||||
|
||||
Accepts either `cfg` and `organization-id` (fetches the org summary from
|
||||
Nitrate) or an already-resolved org summary map."
|
||||
Accepts either `cfg` and `organization-id` (fetches the organization summary from
|
||||
Nitrate) or an already-resolved organization summary map."
|
||||
([cfg organization-id]
|
||||
(get-org-team-ids (nitrate/call cfg :get-org-summary {:organization-id organization-id})))
|
||||
([org-summary]
|
||||
(->> (:teams org-summary)
|
||||
(get-organization-team-ids (nitrate/call cfg :get-organization-summary {:organization-id organization-id})))
|
||||
([organization-summary]
|
||||
(->> (:teams organization-summary)
|
||||
(map :id)
|
||||
(filter uuid?)
|
||||
(vec))))
|
||||
|
||||
(defn get-org-invitations
|
||||
"Fetch valid org-level and team-level invitations for an organization."
|
||||
(defn get-organization-invitations
|
||||
"Fetch valid organization-level and team-level invitations for an organization."
|
||||
[conn organization-id team-ids]
|
||||
(let [ids-array (db/create-array conn "uuid" team-ids)]
|
||||
(db/exec! conn [sql:get-org-invitations organization-id ids-array])))
|
||||
(db/exec! conn [sql:get-organization-invitations organization-id ids-array])))
|
||||
|
||||
(defn get-team-invitation-emails
|
||||
"Return distinct valid team invitation recipient emails."
|
||||
|
||||
@ -16,17 +16,17 @@
|
||||
;;TODO There is a bug on dashboard with teams notifications.
|
||||
;;For now we send it to uuid/zero instead of team-id
|
||||
:topic uuid/zero
|
||||
:message {:type :team-org-change
|
||||
:message {:type :team-organization-change
|
||||
:team team
|
||||
:notification notification})))
|
||||
|
||||
|
||||
(defn notify-user-org-change
|
||||
(defn notify-user-organization-change
|
||||
[cfg profile-id organization-id organization-name notification]
|
||||
(let [msgbus (::mbus/msgbus cfg)]
|
||||
(mbus/pub! msgbus
|
||||
:topic profile-id
|
||||
:message {:type :user-org-change
|
||||
:message {:type :user-organization-change
|
||||
:topic profile-id
|
||||
:organization-id organization-id
|
||||
:organization-name organization-name
|
||||
|
||||
@ -42,15 +42,15 @@
|
||||
:token "test-token"
|
||||
:organization organization})
|
||||
|
||||
(t/deftest invite-to-org-includes-sso-notice-when-active
|
||||
(let [result (emails/render emails/invite-to-org
|
||||
(t/deftest invite-to-organization-includes-sso-notice-when-active
|
||||
(let [result (emails/render emails/invite-to-organization
|
||||
(invite-email-params {:name "Acme Inc"
|
||||
:sso-active true}))]
|
||||
(t/is (str/includes? (email-text-body result) sso-notice-snippet))
|
||||
(t/is (str/includes? (get-in result [:body "text/html"]) sso-notice-snippet))))
|
||||
|
||||
(t/deftest invite-to-org-omits-sso-notice-when-inactive
|
||||
(let [result (emails/render emails/invite-to-org
|
||||
(t/deftest invite-to-organization-omits-sso-notice-when-inactive
|
||||
(let [result (emails/render emails/invite-to-organization
|
||||
(invite-email-params {:name "Acme Inc"
|
||||
:sso-active false}))]
|
||||
(t/is (not (str/includes? (email-text-body result) sso-notice-snippet)))
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -4,7 +4,7 @@
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns backend-tests.rpc-org-owner-permissions-test
|
||||
(ns backend-tests.rpc-organization-owner-permissions-test
|
||||
(:require
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
@ -17,37 +17,37 @@
|
||||
(t/use-fixtures :once th/state-init)
|
||||
(t/use-fixtures :each th/database-reset)
|
||||
|
||||
(defn- org-data
|
||||
[org-id owner-id]
|
||||
{:id org-id
|
||||
(defn- organization-data
|
||||
[organization-id owner-id]
|
||||
{:id organization-id
|
||||
:name "Acme"
|
||||
:slug "acme"
|
||||
:owner-id owner-id
|
||||
:avatar-bg-url "http://example.com/avatar.png"
|
||||
:permissions {}})
|
||||
|
||||
(defn- with-org-owner-access
|
||||
[{:keys [org-owner-id org-id team-id]} f]
|
||||
(defn- with-organization-owner-access
|
||||
[{:keys [organization-owner-id organization-id team-id]} f]
|
||||
(with-redefs [cf/flags (conj cf/flags :nitrate)
|
||||
nitrate/organization-owner-of-team?
|
||||
(fn [_cfg profile-id candidate-team-id]
|
||||
(and (= org-owner-id profile-id)
|
||||
(and (= organization-owner-id profile-id)
|
||||
(= team-id candidate-team-id)))
|
||||
|
||||
nitrate/call
|
||||
(fn [_cfg method params]
|
||||
(case method
|
||||
:get-owned-orgs
|
||||
[{:id org-id
|
||||
:get-owned-organizations
|
||||
[{:id organization-id
|
||||
:name "Acme"
|
||||
:owner-id org-owner-id
|
||||
:owner-id organization-owner-id
|
||||
:teams [{:id team-id :is-your-penpot false}]}]
|
||||
|
||||
:get-team-org
|
||||
:get-team-organization
|
||||
(if (= team-id (:team-id params))
|
||||
{:id team-id
|
||||
:is-your-penpot false
|
||||
:organization (org-data org-id org-owner-id)}
|
||||
:organization (organization-data organization-id organization-owner-id)}
|
||||
{:id (:team-id params)
|
||||
:is-your-penpot false
|
||||
:organization nil})))]
|
||||
@ -67,41 +67,41 @@
|
||||
(filter #(= profile-id (:topic %)))
|
||||
(mapv :message)))
|
||||
|
||||
(t/deftest org-owner-access-disabled-without-nitrate-flag
|
||||
(t/deftest organization-owner-access-disabled-without-nitrate-flag
|
||||
(let [team-owner (th/create-profile* 1)
|
||||
org-owner (th/create-profile* 2)
|
||||
organization-owner (th/create-profile* 2)
|
||||
target-team (th/create-team* 1 {:profile-id (:id team-owner)})]
|
||||
|
||||
(let [out (th/command! {::th/type :get-projects
|
||||
::rpc/profile-id (:id org-owner)
|
||||
::rpc/profile-id (:id organization-owner)
|
||||
:team-id (:id target-team)})
|
||||
error (:error out)]
|
||||
(t/is (th/ex-info? error))
|
||||
(t/is (th/ex-of-type? error :not-found)))))
|
||||
|
||||
(t/deftest non-member-org-owner-gets-viewer-access-to-org-team
|
||||
(t/deftest non-member-organization-owner-gets-viewer-access-to-organization-team
|
||||
(let [team-owner (th/create-profile* 1)
|
||||
org-owner (th/create-profile* 2)
|
||||
organization-owner (th/create-profile* 2)
|
||||
target-team (th/create-team* 1 {:profile-id (:id team-owner)})
|
||||
project (th/create-project* 1 {:profile-id (:id team-owner)
|
||||
:team-id (:id target-team)})
|
||||
file (th/create-file* 1 {:profile-id (:id team-owner)
|
||||
:project-id (:id project)})
|
||||
org-id (uuid/next)]
|
||||
organization-id (uuid/next)]
|
||||
|
||||
(with-org-owner-access {:org-owner-id (:id org-owner)
|
||||
:org-id org-id
|
||||
:team-id (:id target-team)}
|
||||
(with-organization-owner-access {:organization-owner-id (:id organization-owner)
|
||||
:organization-id organization-id
|
||||
:team-id (:id target-team)}
|
||||
(fn []
|
||||
;; The team is not listed for a non-member, even though the org
|
||||
;; The team is not listed for a non-member, even though the organization
|
||||
;; owner can access it directly.
|
||||
(let [out (th/command! {::th/type :get-teams
|
||||
::rpc/profile-id (:id org-owner)})]
|
||||
::rpc/profile-id (:id organization-owner)})]
|
||||
(t/is (nil? (:error out)))
|
||||
(t/is (not-any? #(= (:id target-team) (:id %)) (:result out))))
|
||||
|
||||
(let [out (th/command! {::th/type :get-team
|
||||
::rpc/profile-id (:id org-owner)
|
||||
::rpc/profile-id (:id organization-owner)
|
||||
:id (:id target-team)})
|
||||
team (:result out)]
|
||||
(t/is (nil? (:error out)))
|
||||
@ -109,54 +109,54 @@
|
||||
(t/is (false? (get-in team [:permissions :is-owner])))
|
||||
(t/is (false? (get-in team [:permissions :is-admin])))
|
||||
(t/is (false? (get-in team [:permissions :can-edit])))
|
||||
(t/is (= org-id (get-in team [:organization :id])))
|
||||
(t/is (= organization-id (get-in team [:organization :id])))
|
||||
(t/is (= "Acme" (get-in team [:organization :name]))))
|
||||
|
||||
(let [out (th/command! {::th/type :get-team-members
|
||||
::rpc/profile-id (:id org-owner)
|
||||
::rpc/profile-id (:id organization-owner)
|
||||
:team-id (:id target-team)})
|
||||
members (:result out)]
|
||||
(t/is (nil? (:error out)))
|
||||
(t/is (some #(= (:id team-owner) (:id %)) members))
|
||||
(t/is (not-any? #(= (:id org-owner) (:id %)) members)))
|
||||
(t/is (not-any? #(= (:id organization-owner) (:id %)) members)))
|
||||
|
||||
(let [out (th/command! {::th/type :get-projects
|
||||
::rpc/profile-id (:id org-owner)
|
||||
::rpc/profile-id (:id organization-owner)
|
||||
:team-id (:id target-team)})]
|
||||
(t/is (nil? (:error out)))
|
||||
(t/is (some #(= (:id project) (:id %)) (:result out))))
|
||||
|
||||
(let [out (th/command! {::th/type :get-file
|
||||
::rpc/profile-id (:id org-owner)
|
||||
::rpc/profile-id (:id organization-owner)
|
||||
:id (:id file)})]
|
||||
(t/is (nil? (:error out)))
|
||||
(t/is (= (:id file) (get-in out [:result :id])))
|
||||
(t/is (false? (get-in out [:result :permissions :can-edit]))))
|
||||
|
||||
(let [out (th/command! {::th/type :rename-project
|
||||
::rpc/profile-id (:id org-owner)
|
||||
::rpc/profile-id (:id organization-owner)
|
||||
:id (:id project)
|
||||
:name "Nope"})
|
||||
error (:error out)]
|
||||
(t/is (th/ex-info? error))
|
||||
(t/is (th/ex-of-type? error :not-found)))))))
|
||||
|
||||
(t/deftest org-owner-member-keeps-team-role
|
||||
(t/deftest organization-owner-member-keeps-team-role
|
||||
(let [team-owner (th/create-profile* 1)
|
||||
org-owner (th/create-profile* 2)
|
||||
organization-owner (th/create-profile* 2)
|
||||
target-team (th/create-team* 1 {:profile-id (:id team-owner)})
|
||||
org-id (uuid/next)]
|
||||
organization-id (uuid/next)]
|
||||
|
||||
(th/create-team-role* {:team-id (:id target-team)
|
||||
:profile-id (:id org-owner)
|
||||
:profile-id (:id organization-owner)
|
||||
:role :editor})
|
||||
|
||||
(with-org-owner-access {:org-owner-id (:id org-owner)
|
||||
:org-id org-id
|
||||
:team-id (:id target-team)}
|
||||
(with-organization-owner-access {:organization-owner-id (:id organization-owner)
|
||||
:organization-id organization-id
|
||||
:team-id (:id target-team)}
|
||||
(fn []
|
||||
(let [out (th/command! {::th/type :get-team
|
||||
::rpc/profile-id (:id org-owner)
|
||||
::rpc/profile-id (:id organization-owner)
|
||||
:id (:id target-team)})
|
||||
team (:result out)]
|
||||
(t/is (nil? (:error out)))
|
||||
@ -164,31 +164,31 @@
|
||||
(t/is (false? (get-in team [:permissions :is-admin])))
|
||||
(t/is (true? (get-in team [:permissions :can-edit]))))))))
|
||||
|
||||
(t/deftest removed-org-owner-is-degraded-to-viewer
|
||||
(t/deftest removed-organization-owner-is-degraded-to-viewer
|
||||
(let [team-owner (th/create-profile* 1)
|
||||
org-owner (th/create-profile* 2)
|
||||
organization-owner (th/create-profile* 2)
|
||||
target-team (th/create-team* 1 {:profile-id (:id team-owner)})
|
||||
org-id (uuid/next)
|
||||
organization-id (uuid/next)
|
||||
messages (atom [])]
|
||||
|
||||
(th/create-team-role* {:team-id (:id target-team)
|
||||
:profile-id (:id org-owner)
|
||||
:profile-id (:id organization-owner)
|
||||
:role :editor})
|
||||
|
||||
(with-org-owner-access {:org-owner-id (:id org-owner)
|
||||
:org-id org-id
|
||||
:team-id (:id target-team)}
|
||||
(with-organization-owner-access {:organization-owner-id (:id organization-owner)
|
||||
:organization-id organization-id
|
||||
:team-id (:id target-team)}
|
||||
(fn []
|
||||
(let [out (with-captured-messages messages
|
||||
#(th/command! {::th/type :delete-team-member
|
||||
::rpc/profile-id (:id team-owner)
|
||||
:team-id (:id target-team)
|
||||
:member-id (:id org-owner)}))]
|
||||
:member-id (:id organization-owner)}))]
|
||||
(t/is (nil? (:error out))))
|
||||
|
||||
;; The org owner keeps read-only access, so they are notified
|
||||
;; The organization owner keeps read-only access, so they are notified
|
||||
;; with a role change instead of being kicked out of the team.
|
||||
(let [notified (messages-for messages (:id org-owner))]
|
||||
(let [notified (messages-for messages (:id organization-owner))]
|
||||
(t/is (= 1 (count notified)))
|
||||
(t/is (= :team-role-change (:type (first notified))))
|
||||
(t/is (= :viewer (:role (first notified))))
|
||||
@ -196,7 +196,7 @@
|
||||
(t/is (not-any? #(= :team-membership-change (:type %)) notified)))
|
||||
|
||||
(let [out (th/command! {::th/type :get-team
|
||||
::rpc/profile-id (:id org-owner)
|
||||
::rpc/profile-id (:id organization-owner)
|
||||
:id (:id target-team)})
|
||||
team (:result out)]
|
||||
(t/is (nil? (:error out)))
|
||||
@ -206,19 +206,19 @@
|
||||
|
||||
(t/deftest removed-regular-member-is-still-kicked-out
|
||||
(let [team-owner (th/create-profile* 1)
|
||||
org-owner (th/create-profile* 2)
|
||||
organization-owner (th/create-profile* 2)
|
||||
member (th/create-profile* 3)
|
||||
target-team (th/create-team* 1 {:profile-id (:id team-owner)})
|
||||
org-id (uuid/next)
|
||||
organization-id (uuid/next)
|
||||
messages (atom [])]
|
||||
|
||||
(th/create-team-role* {:team-id (:id target-team)
|
||||
:profile-id (:id member)
|
||||
:role :editor})
|
||||
|
||||
(with-org-owner-access {:org-owner-id (:id org-owner)
|
||||
:org-id org-id
|
||||
:team-id (:id target-team)}
|
||||
(with-organization-owner-access {:organization-owner-id (:id organization-owner)
|
||||
:organization-id organization-id
|
||||
:team-id (:id target-team)}
|
||||
(fn []
|
||||
(let [out (with-captured-messages messages
|
||||
#(th/command! {::th/type :delete-team-member
|
||||
@ -110,24 +110,24 @@
|
||||
audit-mock {:target 'app.loggers.audit/submit :return nil}]
|
||||
(let [owner (th/create-profile* 101 {:is-active true})
|
||||
invitee (th/create-profile* 102 {:is-active true})
|
||||
org-team (th/create-team* 101 {:profile-id (:id owner)})
|
||||
organization-team (th/create-team* 101 {:profile-id (:id owner)})
|
||||
plain-team (th/create-team* 102 {:profile-id (:id owner)})
|
||||
org-id (uuid/random)
|
||||
org {:id org-id
|
||||
:name "Acme"
|
||||
:slug "acme"
|
||||
:owner-id (:id owner)
|
||||
:avatar-bg-url "https://example.com/avatar.svg"
|
||||
:permissions {:new-team-members "anyone"}}
|
||||
organization-id (uuid/random)
|
||||
organization {:id organization-id
|
||||
:name "Acme"
|
||||
:slug "acme"
|
||||
:owner-id (:id owner)
|
||||
:avatar-bg-url "https://example.com/avatar.svg"
|
||||
:permissions {:new-team-members "anyone"}}
|
||||
nitrate-call
|
||||
(fn [_cfg method params]
|
||||
(case method
|
||||
:get-team-org
|
||||
(if (= (:team-id params) (:id org-team))
|
||||
{:organization org :is-your-penpot false}
|
||||
:get-team-organization
|
||||
(if (= (:team-id params) (:id organization-team))
|
||||
{:organization organization :is-your-penpot false}
|
||||
{:organization nil :is-your-penpot false})
|
||||
|
||||
:get-org-members
|
||||
:get-organization-members
|
||||
[(:id invitee)]
|
||||
|
||||
nil))
|
||||
@ -139,21 +139,21 @@
|
||||
:emails [email]}))]
|
||||
(with-redefs [cf/flags (conj cf/flags :nitrate :email-verification)
|
||||
nitrate/call nitrate-call]
|
||||
(t/is (th/success? (invite! org-team (:email invitee))))
|
||||
(t/is (th/success? (invite! org-team (:email invitee))))
|
||||
(t/is (th/success? (invite! organization-team (:email invitee))))
|
||||
(t/is (th/success? (invite! organization-team (:email invitee))))
|
||||
(t/is (th/success? (invite! plain-team "external@example.com"))))
|
||||
|
||||
(let [events (mapv second (:call-args-list @audit-mock))
|
||||
create-org (first (filter #(and (= "create-team-invitation" (:name %))
|
||||
(= (:email invitee)
|
||||
(get-in % [:props :member-email])))
|
||||
events))
|
||||
update-org (first (filter #(= "update-team-invitation" (:name %)) events))
|
||||
create-organization (first (filter #(and (= "create-team-invitation" (:name %))
|
||||
(= (:email invitee)
|
||||
(get-in % [:props :member-email])))
|
||||
events))
|
||||
update-organization (first (filter #(= "update-team-invitation" (:name %)) events))
|
||||
create-plain (first (filter #(and (= "create-team-invitation" (:name %))
|
||||
(= "external@example.com"
|
||||
(get-in % [:props :member-email])))
|
||||
events))]
|
||||
(doseq [event [create-org update-org]]
|
||||
(doseq [event [create-organization update-organization]]
|
||||
(t/is (true? (get-in event [:props :team-belongs-to-organization])))
|
||||
(t/is (true? (get-in event [:props :adds-invitee-to-organization])))
|
||||
(t/is (true? (get-in event [:props :invitee-already-organization-member]))))
|
||||
@ -486,11 +486,11 @@
|
||||
nitrate/call
|
||||
(fn [_cfg method _params]
|
||||
(case method
|
||||
:get-org-membership {:organization-id organization-id
|
||||
:is-member false}
|
||||
:get-org-members [(:id inviter) (uuid/random) (uuid/random)]
|
||||
:get-organization-membership {:organization-id organization-id
|
||||
:is-member false}
|
||||
:get-organization-members [(:id inviter) (uuid/random) (uuid/random)]
|
||||
nil))
|
||||
teams/initialize-user-in-nitrate-org
|
||||
teams/initialize-user-in-nitrate-organization
|
||||
(fn [& _] default-team-id)]
|
||||
(let [out (verify! direct-token)]
|
||||
(t/is (th/success? out))
|
||||
@ -531,10 +531,10 @@
|
||||
nitrate/call
|
||||
(fn [_cfg method _params]
|
||||
(case method
|
||||
:get-org-membership-by-team {:organization-id organization-id
|
||||
:is-member false}
|
||||
:get-org-members (into [(:id inviter)]
|
||||
(repeatedly 4 uuid/random))
|
||||
:get-organization-membership-by-team {:organization-id organization-id
|
||||
:is-member false}
|
||||
:get-organization-members (into [(:id inviter)]
|
||||
(repeatedly 4 uuid/random))
|
||||
nil))
|
||||
teams/add-profile-to-team! (fn [& _] nil)]
|
||||
(let [out (verify! team-token)]
|
||||
@ -574,9 +574,9 @@
|
||||
nitrate/call
|
||||
(fn [_cfg method _params]
|
||||
(case method
|
||||
:get-org-membership-by-team {:organization-id organization-id
|
||||
:is-member true}
|
||||
:get-org-members (throw (ex-info "unexpected member count" {}))
|
||||
:get-organization-membership-by-team {:organization-id organization-id
|
||||
:is-member true}
|
||||
:get-organization-members (throw (ex-info "unexpected member count" {}))
|
||||
nil))
|
||||
teams/add-profile-to-team! (fn [& _] nil)]
|
||||
(let [out (verify! team-token)]
|
||||
|
||||
@ -14,29 +14,29 @@
|
||||
:new-team-members "anyone"})
|
||||
|
||||
(defn- can-create-team?
|
||||
[{:keys [is-org-owner? permission-value]}]
|
||||
(or is-org-owner?
|
||||
[{:keys [is-organization-owner? permission-value]}]
|
||||
(or is-organization-owner?
|
||||
(= permission-value "any")))
|
||||
|
||||
(defn- can-delete-team?
|
||||
[{:keys [is-org-owner? permission-value team-perms]}]
|
||||
[{:keys [is-organization-owner? permission-value team-perms]}]
|
||||
(cond
|
||||
;; Org owners can always delete teams inside their organizations.
|
||||
is-org-owner?
|
||||
;; Organization owners can always delete teams inside their organizations.
|
||||
is-organization-owner?
|
||||
true
|
||||
(= permission-value "onlyOwners")
|
||||
(boolean (:is-owner team-perms))
|
||||
:else false))
|
||||
|
||||
(defn- can-move-team?
|
||||
[{:keys [permission-value target-org-same-owner?]}]
|
||||
[{:keys [permission-value target-organization-same-owner?]}]
|
||||
(cond
|
||||
(= permission-value "never")
|
||||
false
|
||||
(= permission-value "always")
|
||||
true
|
||||
(= permission-value "myOrganizations")
|
||||
(true? target-org-same-owner?)
|
||||
(true? target-organization-same-owner?)
|
||||
:else false))
|
||||
|
||||
(defn- can-invite-to-team?
|
||||
@ -67,36 +67,36 @@
|
||||
:add-anybody-to-team {:permission-key :new-team-members
|
||||
:check-fn can-add-anybody-to-team?}})
|
||||
|
||||
(defn- normalize-org-permissions
|
||||
[org-perms]
|
||||
(merge defaults (or (:permissions org-perms) {})))
|
||||
(defn- normalize-organization-permissions
|
||||
[organization-perms]
|
||||
(merge defaults (or (:permissions organization-perms) {})))
|
||||
|
||||
(defn- owner?
|
||||
[org-perms profile-id]
|
||||
(= profile-id (:owner-id org-perms)))
|
||||
[organization-perms profile-id]
|
||||
(= profile-id (:owner-id organization-perms)))
|
||||
|
||||
(defn allowed?
|
||||
"Returns true only for explicitly allowed actions (fail-closed)."
|
||||
[action {:keys [org-perms profile-id team-perms target-org-same-owner?]}]
|
||||
[action {:keys [organization-perms profile-id team-perms target-organization-same-owner?]}]
|
||||
(let [{:keys [permission-key check-fn] :as rule}
|
||||
(get action-rules action)
|
||||
permissions (normalize-org-permissions org-perms)
|
||||
is-org-owner? (owner? org-perms profile-id)
|
||||
permissions (normalize-organization-permissions organization-perms)
|
||||
is-organization-owner? (owner? organization-perms profile-id)
|
||||
permission-value (get permissions permission-key)]
|
||||
(cond
|
||||
(nil? rule) false
|
||||
:else (boolean (check-fn {:is-org-owner? is-org-owner?
|
||||
:else (boolean (check-fn {:is-organization-owner? is-organization-owner?
|
||||
:permission-value permission-value
|
||||
:team-perms team-perms
|
||||
:target-org-same-owner? target-org-same-owner?})))))
|
||||
:target-organization-same-owner? target-organization-same-owner?})))))
|
||||
|
||||
(defn can-send-invitations?
|
||||
[{:keys [nitrate-enabled? organization profile-id team-permissions]}]
|
||||
(let [in-org? (and nitrate-enabled? organization)]
|
||||
(if in-org?
|
||||
(let [in-organization? (and nitrate-enabled? organization)]
|
||||
(if in-organization?
|
||||
(allowed? :send-invitations
|
||||
{:org-perms {:owner-id (:owner-id organization)
|
||||
:permissions (:permissions organization)}
|
||||
{:organization-perms {:owner-id (:owner-id organization)
|
||||
:permissions (:permissions organization)}
|
||||
:profile-id profile-id
|
||||
:team-perms team-permissions})
|
||||
(or (boolean (:is-owner team-permissions))
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
[app.common.types.nitrate-permissions :as nitrate-perms]
|
||||
[clojure.test :as t]))
|
||||
|
||||
(def org-perms
|
||||
(def organization-perms
|
||||
{:owner-id :owner
|
||||
:permissions {:create-teams "any"
|
||||
:delete-teams "onlyOwners"
|
||||
@ -17,162 +17,162 @@
|
||||
|
||||
(t/deftest unknown-action-is-denied
|
||||
(t/is (false? (nitrate-perms/allowed? :unknown
|
||||
{:org-perms org-perms
|
||||
{:organization-perms organization-perms
|
||||
:profile-id :member
|
||||
:team-perms {:is-admin true}}))))
|
||||
|
||||
(t/deftest org-owner-is-allowed-for-create-and-delete
|
||||
(t/deftest organization-owner-is-allowed-for-create-and-delete
|
||||
(t/is (true? (nitrate-perms/allowed? :create-team
|
||||
{:org-perms org-perms
|
||||
{:organization-perms organization-perms
|
||||
:profile-id :owner
|
||||
:team-perms {:is-admin false}})))
|
||||
(t/is (true? (nitrate-perms/allowed? :delete-team
|
||||
{:org-perms org-perms
|
||||
{:organization-perms organization-perms
|
||||
:profile-id :owner
|
||||
:team-perms {:is-admin false}}))))
|
||||
|
||||
(t/deftest create-team-permission-rules
|
||||
(t/is (true? (nitrate-perms/allowed? :create-team
|
||||
{:org-perms org-perms
|
||||
{:organization-perms organization-perms
|
||||
:profile-id :member
|
||||
:team-perms {:is-admin false}})))
|
||||
(t/is (false? (nitrate-perms/allowed? :create-team
|
||||
{:org-perms (assoc org-perms :permissions {:create-teams "none"
|
||||
:delete-teams "onlyOwners"})
|
||||
{:organization-perms (assoc organization-perms :permissions {:create-teams "none"
|
||||
:delete-teams "onlyOwners"})
|
||||
:profile-id :member
|
||||
:team-perms {:is-admin false}}))))
|
||||
|
||||
(t/deftest delete-team-onlyowners-allows-only-team-owners
|
||||
(t/is (true? (nitrate-perms/allowed? :delete-team
|
||||
{:org-perms org-perms
|
||||
{:organization-perms organization-perms
|
||||
:profile-id :member
|
||||
:team-perms {:is-owner true :is-admin true}})))
|
||||
(t/is (false? (nitrate-perms/allowed? :delete-team
|
||||
{:org-perms org-perms
|
||||
{:organization-perms organization-perms
|
||||
:profile-id :member
|
||||
:team-perms {:is-admin true}})))
|
||||
(t/is (false? (nitrate-perms/allowed? :delete-team
|
||||
{:org-perms (assoc org-perms :permissions {:create-teams "any"
|
||||
:delete-teams "invalid-value"})
|
||||
{:organization-perms (assoc organization-perms :permissions {:create-teams "any"
|
||||
:delete-teams "invalid-value"})
|
||||
:profile-id :member
|
||||
:team-perms {:is-admin true}}))))
|
||||
|
||||
(t/deftest delete-team-onlyme-still-allows-org-owner
|
||||
(let [only-me-org (assoc org-perms :permissions {:create-teams "any"
|
||||
:delete-teams "onlyMe"})]
|
||||
(t/deftest delete-team-onlyme-still-allows-organization-owner
|
||||
(let [only-me-organization (assoc organization-perms :permissions {:create-teams "any"
|
||||
:delete-teams "onlyMe"})]
|
||||
(t/is (true? (nitrate-perms/allowed? :delete-team
|
||||
{:org-perms only-me-org
|
||||
{:organization-perms only-me-organization
|
||||
:profile-id :owner
|
||||
:team-perms {:is-owner false :is-admin false}})))
|
||||
(t/is (false? (nitrate-perms/allowed? :delete-team
|
||||
{:org-perms only-me-org
|
||||
{:organization-perms only-me-organization
|
||||
:profile-id :member
|
||||
:team-perms {:is-owner true :is-admin true}})))))
|
||||
|
||||
(t/deftest move-team-always-allows-any-org-owner-or-all-users
|
||||
(let [always-org (assoc org-perms :permissions {:create-teams "any"
|
||||
:delete-teams "onlyOwners"
|
||||
:move-teams "always"})]
|
||||
;; Org owner should always be allowed
|
||||
(t/deftest move-team-always-allows-any-organization-owner-or-all-users
|
||||
(let [always-organization (assoc organization-perms :permissions {:create-teams "any"
|
||||
:delete-teams "onlyOwners"
|
||||
:move-teams "always"})]
|
||||
;; Organization owner should always be allowed
|
||||
(t/is (true? (nitrate-perms/allowed? :move-team
|
||||
{:org-perms always-org
|
||||
{:organization-perms always-organization
|
||||
:profile-id :owner
|
||||
:team-perms {}})))
|
||||
;; Regular member should be allowed when move-teams is "always"
|
||||
(t/is (true? (nitrate-perms/allowed? :move-team
|
||||
{:org-perms always-org
|
||||
{:organization-perms always-organization
|
||||
:profile-id :member
|
||||
:team-perms {}})))))
|
||||
|
||||
(t/deftest move-team-myorganizations-allows-only-within-same-owner
|
||||
(let [my-orgs (assoc org-perms :permissions {:create-teams "any"
|
||||
:delete-teams "onlyOwners"
|
||||
:move-teams "myOrganizations"})]
|
||||
;; Org owner must also stay within same-owner organizations
|
||||
(let [my-organizations (assoc organization-perms :permissions {:create-teams "any"
|
||||
:delete-teams "onlyOwners"
|
||||
:move-teams "myOrganizations"})]
|
||||
;; Organization owner must also stay within same-owner organizations
|
||||
(t/is (false? (nitrate-perms/allowed? :move-team
|
||||
{:org-perms my-orgs
|
||||
{:organization-perms my-organizations
|
||||
:profile-id :owner
|
||||
:team-perms {}
|
||||
:target-org-same-owner? false})))
|
||||
:target-organization-same-owner? false})))
|
||||
(t/is (true? (nitrate-perms/allowed? :move-team
|
||||
{:org-perms my-orgs
|
||||
{:organization-perms my-organizations
|
||||
:profile-id :owner
|
||||
:team-perms {}
|
||||
:target-org-same-owner? true})))
|
||||
:target-organization-same-owner? true})))
|
||||
;; Regular member should be allowed only if target has same owner
|
||||
(t/is (true? (nitrate-perms/allowed? :move-team
|
||||
{:org-perms my-orgs
|
||||
{:organization-perms my-organizations
|
||||
:profile-id :member
|
||||
:team-perms {}
|
||||
:target-org-same-owner? true})))
|
||||
:target-organization-same-owner? true})))
|
||||
(t/is (false? (nitrate-perms/allowed? :move-team
|
||||
{:org-perms my-orgs
|
||||
{:organization-perms my-organizations
|
||||
:profile-id :member
|
||||
:team-perms {}
|
||||
:target-org-same-owner? false})))))
|
||||
:target-organization-same-owner? false})))))
|
||||
|
||||
(t/deftest move-team-never-denies-all
|
||||
(let [never-org (assoc org-perms :permissions {:create-teams "any"
|
||||
:delete-teams "onlyOwners"
|
||||
:move-teams "never"})]
|
||||
;; Even org owner should be denied
|
||||
(let [never-organization (assoc organization-perms :permissions {:create-teams "any"
|
||||
:delete-teams "onlyOwners"
|
||||
:move-teams "never"})]
|
||||
;; Even organization owner should be denied
|
||||
(t/is (false? (nitrate-perms/allowed? :move-team
|
||||
{:org-perms never-org
|
||||
{:organization-perms never-organization
|
||||
:profile-id :owner
|
||||
:team-perms {}})))
|
||||
;; Regular member should be denied
|
||||
(t/is (false? (nitrate-perms/allowed? :move-team
|
||||
{:org-perms never-org
|
||||
{:organization-perms never-organization
|
||||
:profile-id :member
|
||||
:team-perms {}})))))
|
||||
|
||||
(t/deftest move-team-defaults-to-always
|
||||
(let [default-org (assoc org-perms :permissions {:create-teams "any"
|
||||
:delete-teams "onlyOwners"})]
|
||||
(let [default-organization (assoc organization-perms :permissions {:create-teams "any"
|
||||
:delete-teams "onlyOwners"})]
|
||||
;; Should default to "always" when not specified
|
||||
(t/is (true? (nitrate-perms/allowed? :move-team
|
||||
{:org-perms default-org
|
||||
{:organization-perms default-organization
|
||||
:profile-id :member
|
||||
:team-perms {}})))))
|
||||
|
||||
(t/deftest send-invitations-defaults-to-owners-and-admins
|
||||
(let [default-org (assoc org-perms :permissions {:create-teams "any"
|
||||
:delete-teams "onlyOwners"})]
|
||||
(let [default-organization (assoc organization-perms :permissions {:create-teams "any"
|
||||
:delete-teams "onlyOwners"})]
|
||||
(t/is (true? (nitrate-perms/allowed? :send-invitations
|
||||
{:org-perms default-org
|
||||
{:organization-perms default-organization
|
||||
:profile-id :owner
|
||||
:team-perms {:is-owner true :is-admin false}})))
|
||||
(t/is (true? (nitrate-perms/allowed? :send-invitations
|
||||
{:org-perms default-org
|
||||
{:organization-perms default-organization
|
||||
:profile-id :member
|
||||
:team-perms {:is-owner false :is-admin true}})))
|
||||
(t/is (false? (nitrate-perms/allowed? :send-invitations
|
||||
{:org-perms default-org
|
||||
{:organization-perms default-organization
|
||||
:profile-id :member
|
||||
:team-perms {:is-owner false :is-admin false}})))))
|
||||
|
||||
(t/deftest send-invitations-owners-allows-only-team-owners
|
||||
(let [only-owners-org (assoc org-perms :permissions {:create-teams "any"
|
||||
:delete-teams "onlyOwners"
|
||||
:send-invitations "owners"})]
|
||||
(let [only-owners-organization (assoc organization-perms :permissions {:create-teams "any"
|
||||
:delete-teams "onlyOwners"
|
||||
:send-invitations "owners"})]
|
||||
(t/is (true? (nitrate-perms/allowed? :send-invitations
|
||||
{:org-perms only-owners-org
|
||||
{:organization-perms only-owners-organization
|
||||
:profile-id :member
|
||||
:team-perms {:is-owner true :is-admin true}})))
|
||||
(t/is (false? (nitrate-perms/allowed? :send-invitations
|
||||
{:org-perms only-owners-org
|
||||
{:organization-perms only-owners-organization
|
||||
:profile-id :owner
|
||||
:team-perms {:is-owner false :is-admin false}})))
|
||||
(t/is (false? (nitrate-perms/allowed? :send-invitations
|
||||
{:org-perms only-owners-org
|
||||
{:organization-perms only-owners-organization
|
||||
:profile-id :member
|
||||
:team-perms {:is-owner false :is-admin true}})))))
|
||||
|
||||
(t/deftest send-invitations-invalid-value-is-denied
|
||||
(let [invalid-org (assoc org-perms :permissions {:create-teams "any"
|
||||
:delete-teams "onlyOwners"
|
||||
:send-invitations "invalid-value"})]
|
||||
(let [invalid-organization (assoc organization-perms :permissions {:create-teams "any"
|
||||
:delete-teams "onlyOwners"
|
||||
:send-invitations "invalid-value"})]
|
||||
(t/is (false? (nitrate-perms/allowed? :send-invitations
|
||||
{:org-perms invalid-org
|
||||
{:organization-perms invalid-organization
|
||||
:profile-id :member
|
||||
:team-perms {:is-owner true :is-admin true}})))))
|
||||
|
||||
@ -245,11 +245,11 @@
|
||||
(rx/of (rt/nav-raw :uri (str redirect-uri)))
|
||||
(rx/empty))))))))
|
||||
|
||||
(defn handle-change-team-org
|
||||
"Handle :team-org-change websocket messages on dashboard and workspace.
|
||||
Updates local team org data and redirects to SSO when required."
|
||||
(defn handle-change-team-organization
|
||||
"Handle :team-organization-change websocket messages on dashboard and workspace.
|
||||
Updates local team organization data and redirects to SSO when required."
|
||||
[{:keys [team notification]}]
|
||||
(ptk/reify ::handle-change-team-org
|
||||
(ptk/reify ::handle-change-team-organization
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [current-team-id (:current-team-id state)
|
||||
@ -279,7 +279,7 @@
|
||||
|
||||
(defn handle-organization-change-sso
|
||||
"Handle :organization-change-sso websocket messages on dashboard and workspace.
|
||||
Redirects to the org SSO login when the current team belongs to that org."
|
||||
Redirects to the organization SSO login when the current team belongs to that organization."
|
||||
[{:keys [organization-id]}]
|
||||
(ptk/reify ::handle-organization-change-sso
|
||||
ptk/WatchEvent
|
||||
@ -287,8 +287,8 @@
|
||||
(when (contains? cf/flags :nitrate)
|
||||
(let [team-id (:current-team-id state)
|
||||
team (dm/get-in state [:teams team-id])
|
||||
org-id (dm/get-in team [:organization :id])]
|
||||
(when (= organization-id org-id)
|
||||
team-organization-id (dm/get-in team [:organization :id])]
|
||||
(when (= organization-id team-organization-id)
|
||||
(check-team-sso team-id)))))))
|
||||
|
||||
|
||||
|
||||
@ -685,9 +685,9 @@
|
||||
(rx/of (dcm/change-team-role params)
|
||||
(modal/hide)))))
|
||||
|
||||
(defn- handle-user-org-change
|
||||
(defn- handle-user-organization-change
|
||||
[{:keys [organization-id organization-name notification]}]
|
||||
(ptk/reify ::handle-user-org-change
|
||||
(ptk/reify ::handle-user-organization-change
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(when (and notification (contains? cf/flags :nitrate))
|
||||
@ -698,7 +698,7 @@
|
||||
:level :info
|
||||
:timeout nil})
|
||||
(dtm/fetch-teams)
|
||||
;; When the user is currently on a team of the org
|
||||
;; When the user is currently on a team of the organization
|
||||
(when (= organization-id (dm/get-in team [:organization :id]))
|
||||
(dcm/go-to-dashboard-recent {:team-id :default}))))))))
|
||||
|
||||
@ -711,22 +711,22 @@
|
||||
(when (contains? cf/flags :nitrate)
|
||||
(let [team-id (:current-team-id state)
|
||||
current-team (dm/get-in state [:teams team-id])
|
||||
current-org-id (dm/get-in current-team [:organization :id])
|
||||
current-organization-id (dm/get-in current-team [:organization :id])
|
||||
teams-set (set teams)
|
||||
notify? (contains? teams-set team-id)
|
||||
fetch? (some (:teams state) teams)
|
||||
go-to-default? (or (some #{team-id} deleted-teams)
|
||||
(= organization-id current-org-id))]
|
||||
(= organization-id current-organization-id))]
|
||||
(rx/concat
|
||||
(when go-to-default? ;; If the user is currently on one of the deleted teams
|
||||
(rx/of (dcm/go-to-dashboard-recent {:team-id :default})))
|
||||
|
||||
(when notify? ;; If the user is currently on one of the org teams
|
||||
(rx/of (ntf/show {:content (tr "dashboard.org-deleted" organization-name)
|
||||
(when notify? ;; If the user is currently on one of the organization teams
|
||||
(rx/of (ntf/show {:content (tr "dashboard.organization-deleted" organization-name)
|
||||
:type :toast
|
||||
:level :info
|
||||
:timeout nil})))
|
||||
(when fetch? ;; If the user belonged to the org
|
||||
(when fetch? ;; If the user belonged to the organization
|
||||
(rx/of (dtm/fetch-teams)))))))))
|
||||
|
||||
(defn- process-message
|
||||
@ -735,8 +735,8 @@
|
||||
:notification (dcm/handle-notification msg)
|
||||
:team-role-change (handle-change-team-role msg)
|
||||
:team-membership-change (dcm/team-membership-change msg)
|
||||
:team-org-change (dcm/handle-change-team-org msg)
|
||||
:user-org-change (handle-user-org-change msg)
|
||||
:team-organization-change (dcm/handle-change-team-organization msg)
|
||||
:user-organization-change (handle-user-organization-change msg)
|
||||
:organization-deleted (handle-organization-deleted msg)
|
||||
:organization-change-sso (dcm/handle-organization-change-sso msg)
|
||||
nil))
|
||||
|
||||
@ -212,7 +212,7 @@
|
||||
(defn lookup-team
|
||||
"The team identified by `team-id`, looked up first in the membership
|
||||
`:teams` map and falling back to the directly-opened `:current-team`.
|
||||
The fallback covers org-owner access to teams the profile is not a
|
||||
The fallback covers organization-owner access to teams the profile is not a
|
||||
member of, which are kept out of `:teams` so they don't leak into the
|
||||
teams listing."
|
||||
([state]
|
||||
|
||||
@ -161,7 +161,7 @@
|
||||
(contains? #{"active" "past_due" "trialing"}
|
||||
(dm/get-in profile [:subscription :status]))))
|
||||
|
||||
(defn leave-org
|
||||
(defn leave-organization
|
||||
[{:keys [id
|
||||
name
|
||||
default-team-id
|
||||
@ -171,7 +171,7 @@
|
||||
organization-member-count-before
|
||||
on-error]}]
|
||||
|
||||
(ptk/reify ::leave-org
|
||||
(ptk/reify ::leave-organization
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [profile-id (dm/get-in state [:profile :id])
|
||||
@ -191,29 +191,29 @@
|
||||
:subscription-status subscription-status})]
|
||||
(rx/concat
|
||||
(rx/of audit-event)
|
||||
(->> (rp/cmd! ::leave-org {:id id
|
||||
:name name
|
||||
:default-team-id default-team-id
|
||||
:teams-to-delete teams-to-delete
|
||||
:teams-to-leave teams-to-leave})
|
||||
(->> (rp/cmd! ::leave-organization {:id id
|
||||
:name name
|
||||
:default-team-id default-team-id
|
||||
:teams-to-delete teams-to-delete
|
||||
:teams-to-leave teams-to-leave})
|
||||
(rx/mapcat
|
||||
(fn [_]
|
||||
(rx/of
|
||||
(dt/fetch-teams)
|
||||
(dcm/go-to-dashboard-recent :team-id profile-team-id)
|
||||
(modal/hide)
|
||||
(ntf/show {:content (tr "dasboard.leave-org.toast" name)
|
||||
(ntf/show {:content (tr "dashboard.leave-organization.toast" name)
|
||||
:type :toast
|
||||
:level :success}))))
|
||||
(rx/catch on-error)))))))
|
||||
|
||||
(defn show-leave-org-modal
|
||||
(defn show-leave-organization-modal
|
||||
[{:keys [organization profile default-team-id leave-fn teams-to-transfer on-error]}]
|
||||
(ptk/reify ::show-leave-org-modal
|
||||
(ptk/reify ::show-leave-organization-modal
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(->> (rp/cmd! ::get-leave-org-summary {:id (:id organization)
|
||||
:default-team-id default-team-id})
|
||||
(->> (rp/cmd! ::get-leave-organization-summary {:id (:id organization)
|
||||
:default-team-id default-team-id})
|
||||
(rx/mapcat
|
||||
(fn [summary]
|
||||
(let [num-teams-to-delete (:teams-to-delete summary)
|
||||
@ -231,7 +231,7 @@
|
||||
(pos? num-teams-to-transfer)
|
||||
(rx/of
|
||||
(modal/show
|
||||
{:type :leave-and-reassign-org
|
||||
{:type :leave-and-reassign-organization
|
||||
:profile profile
|
||||
:teams-to-transfer teams-to-transfer
|
||||
:num-teams-to-delete num-teams-to-delete
|
||||
@ -242,28 +242,28 @@
|
||||
(pos? num-teams-to-detach))
|
||||
(rx/of (modal/show
|
||||
{:type :confirm
|
||||
:title (tr "modals.before-leave-org.title" (:name organization))
|
||||
:message (tr "modals.before-leave-org.message")
|
||||
:accept-label (tr "modals.leave-org-confirm.accept")
|
||||
:title (tr "modals.before-leave-organization.title" (:name organization))
|
||||
:message (tr "modals.before-leave-organization.message")
|
||||
:accept-label (tr "modals.leave-organization-confirm.accept")
|
||||
:on-accept leave-fn
|
||||
:error-msg (tr "modals.before-leave-org.warning")}))
|
||||
:error-msg (tr "modals.before-leave-organization.warning")}))
|
||||
|
||||
:else
|
||||
(rx/of (modal/show
|
||||
{:type :confirm
|
||||
:title (tr "modals.leave-org-confirm.title" (:name organization))
|
||||
:message (tr "modals.leave-org-confirm.message")
|
||||
:accept-label (tr "modals.leave-org-confirm.accept")
|
||||
:title (tr "modals.leave-organization-confirm.title" (:name organization))
|
||||
:message (tr "modals.leave-organization-confirm.message")
|
||||
:accept-label (tr "modals.leave-organization-confirm.accept")
|
||||
:on-accept leave-fn}))))))
|
||||
(rx/catch on-error)))))
|
||||
|
||||
|
||||
(defn remove-team-from-org
|
||||
(defn remove-team-from-organization
|
||||
[{:keys [team-id organization-id organization-name] :as params}]
|
||||
(ptk/reify ::remove-team-from-org
|
||||
(ptk/reify ::remove-team-from-organization
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(->> (rp/cmd! ::remove-team-from-org {:team-id team-id :organization-id organization-id :organization-name organization-name})
|
||||
(->> (rp/cmd! ::remove-team-from-organization {:team-id team-id :organization-id organization-id :organization-name organization-name})
|
||||
(rx/mapcat
|
||||
(fn [_]
|
||||
(rx/of (dt/fetch-teams)
|
||||
@ -272,40 +272,40 @@
|
||||
(fn [cause]
|
||||
(let [code (-> cause ex-data :code)]
|
||||
(if (= code :not-allowed)
|
||||
(rx/of (modal/show :no-permission-modal {:type :no-orgs-change}))
|
||||
(rx/of (modal/show :no-permission-modal {:type :no-organizations-change}))
|
||||
(rx/throw cause)))))))))
|
||||
|
||||
(defn show-remove-team-from-org-modal
|
||||
"Fetches fresh team/org data, then shows the remove-from-org confirmation
|
||||
(defn show-remove-team-from-organization-modal
|
||||
"Fetches fresh team/organization data, then shows the remove-from-organization confirmation
|
||||
modal or the no-permission modal if the move-team permission blocks it."
|
||||
[{:keys [team-id]}]
|
||||
(ptk/reify ::show-remove-team-from-org-modal
|
||||
(ptk/reify ::show-remove-team-from-organization-modal
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [profile-id (dm/get-in state [:profile :id])]
|
||||
(dt/with-refreshed-team team-id
|
||||
(fn [team]
|
||||
(let [source-org (:organization team)
|
||||
(let [source-organization (:organization team)
|
||||
can-move? (nitrate-perms/allowed?
|
||||
:move-team
|
||||
{:org-perms {:owner-id (:owner-id source-org)
|
||||
:permissions (:permissions source-org)}
|
||||
{:organization-perms {:owner-id (:owner-id source-organization)
|
||||
:permissions (:permissions source-organization)}
|
||||
:profile-id profile-id
|
||||
:team-perms (:permissions team)
|
||||
:target-org-same-owner? false})]
|
||||
:target-organization-same-owner? false})]
|
||||
(rx/of (if can-move?
|
||||
(modal/show
|
||||
{:type :confirm
|
||||
:title (tr "modals.remove-team-org.title")
|
||||
:message (tr "modals.remove-team-org.text" (:name team) (:name source-org))
|
||||
:hint (tr "modals.remove-team-org.info")
|
||||
:title (tr "modals.remove-team-organization.title")
|
||||
:message (tr "modals.remove-team-organization.text" (:name team) (:name source-organization))
|
||||
:hint (tr "modals.remove-team-organization.info")
|
||||
:hint-level :default
|
||||
:accept-label (tr "modals.remove-team-org.accept")
|
||||
:on-accept #(st/emit! (remove-team-from-org {:team-id team-id
|
||||
:organization-id (:id source-org)
|
||||
:organization-name (:name source-org)}))
|
||||
:accept-label (tr "modals.remove-team-organization.accept")
|
||||
:on-accept #(st/emit! (remove-team-from-organization {:team-id team-id
|
||||
:organization-id (:id source-organization)
|
||||
:organization-name (:name source-organization)}))
|
||||
:accept-style :danger})
|
||||
(modal/show :no-permission-modal {:type :no-orgs-change}))))))))))
|
||||
(modal/show :no-permission-modal {:type :no-organizations-change}))))))))))
|
||||
|
||||
|
||||
(defn add-team-to-organization
|
||||
@ -359,24 +359,24 @@
|
||||
(rx/empty)))))))))))
|
||||
|
||||
|
||||
(defn- fetch-orgs-allowed
|
||||
"Returns an rx observable of an `orgs-allowed` map (org-id -> boolean).
|
||||
Orgs where :add-anybody-to-team is permitted are pre-approved;
|
||||
the rest are verified via :all-team-members-in-orgs."
|
||||
[team-id orgs]
|
||||
(let [add-anybody-orgs (filterv #(nitrate-perms/allowed? :add-anybody-to-team {:org-perms %}) orgs)
|
||||
orgs-to-check (filterv #(not (nitrate-perms/allowed? :add-anybody-to-team {:org-perms %})) orgs)
|
||||
org-ids-to-check (mapv :id orgs-to-check)]
|
||||
(if (empty? org-ids-to-check)
|
||||
(rx/of (into {} (map (fn [org] [(:id org) true])) orgs))
|
||||
(->> (rp/cmd! :all-team-members-in-orgs {:team-id team-id :organization-ids org-ids-to-check})
|
||||
(rx/map (fn [checked-orgs]
|
||||
(merge (into {} (map (fn [org] [(:id org) true])) add-anybody-orgs)
|
||||
checked-orgs)))))))
|
||||
(defn- fetch-organizations-allowed
|
||||
"Returns an rx observable of an `organizations-allowed` map (organization-id -> boolean).
|
||||
Organizations where :add-anybody-to-team is permitted are pre-approved;
|
||||
the rest are verified via :all-team-members-in-organizations."
|
||||
[team-id organizations]
|
||||
(let [add-anybody-organizations (filterv #(nitrate-perms/allowed? :add-anybody-to-team {:organization-perms %}) organizations)
|
||||
organizations-to-check (filterv #(not (nitrate-perms/allowed? :add-anybody-to-team {:organization-perms %})) organizations)
|
||||
organization-ids-to-check (mapv :id organizations-to-check)]
|
||||
(if (empty? organization-ids-to-check)
|
||||
(rx/of (into {} (map (fn [organization] [(:id organization) true])) organizations))
|
||||
(->> (rp/cmd! :all-team-members-in-organizations {:team-id team-id :organization-ids organization-ids-to-check})
|
||||
(rx/map (fn [checked-organizations]
|
||||
(merge (into {} (map (fn [organization] [(:id organization) true])) add-anybody-organizations)
|
||||
checked-organizations)))))))
|
||||
|
||||
(defn show-add-team-to-organization-modal
|
||||
"Fetches fresh team/org data, then shows the add-to-org modal
|
||||
restricted to orgs where the user has permission, or the no-permission
|
||||
"Fetches fresh team/organization data, then shows the add-to-organization modal
|
||||
restricted to organizations where the user has permission, or the no-permission
|
||||
modal if none qualify."
|
||||
[{:keys [team-id]}]
|
||||
(ptk/reify ::show-add-team-to-organization-modal
|
||||
@ -386,114 +386,114 @@
|
||||
(->> (rp/cmd! :get-teams)
|
||||
(rx/mapcat
|
||||
(fn [teams]
|
||||
(let [all-orgs (map dt/team->organization
|
||||
(filter #(and (:is-default %) (:organization %)) teams))
|
||||
orgs (filter (fn [org]
|
||||
(let [perm (dm/get-in org [:permissions :create-teams])
|
||||
is-own? (= profile-id (:owner-id org))]
|
||||
(or (= perm "any") is-own?))) all-orgs)
|
||||
(let [all-organizations (map dt/team->organization
|
||||
(filter #(and (:is-default %) (:organization %)) teams))
|
||||
organizations (filter (fn [organization]
|
||||
(let [perm (dm/get-in organization [:permissions :create-teams])
|
||||
is-own? (= profile-id (:owner-id organization))]
|
||||
(or (= perm "any") is-own?))) all-organizations)
|
||||
team (first (filter #(= (:id %) team-id) teams))
|
||||
on-confirm (fn [organization-id]
|
||||
(st/emit! (add-team-to-organization {:team-id team-id
|
||||
:organization-id organization-id})))
|
||||
show-select-modal
|
||||
(fn [orgs-allowed]
|
||||
(let [has-filtered? (< (count orgs) (count all-orgs))
|
||||
(fn [organizations-allowed]
|
||||
(let [has-filtered? (< (count organizations) (count all-organizations))
|
||||
extra-props (when has-filtered?
|
||||
{:info-message-key "dashboard.select-org-modal.permission-info"})]
|
||||
{:info-message-key "dashboard.select-organization-modal.permission-info"})]
|
||||
(modal/show :select-organization-modal
|
||||
(merge {:organizations orgs
|
||||
:orgs-allowed orgs-allowed
|
||||
(merge {:organizations organizations
|
||||
:organizations-allowed organizations-allowed
|
||||
:current-organization-id (dm/get-in team [:organization :id])
|
||||
:on-confirm on-confirm
|
||||
:team-id team-id
|
||||
:title-key "dashboard.select-org-modal.title"
|
||||
:choose-key "dashboard.select-org-modal.choose"
|
||||
:placeholder-key "dashboard.select-org-modal.select"
|
||||
:accept-key "dashboard.select-org-modal.accept"
|
||||
:title-key "dashboard.select-organization-modal.title"
|
||||
:choose-key "dashboard.select-organization-modal.choose"
|
||||
:placeholder-key "dashboard.select-organization-modal.select"
|
||||
:accept-key "dashboard.select-organization-modal.accept"
|
||||
:cancel-key "labels.cancel"}
|
||||
extra-props))))]
|
||||
(if (empty? orgs)
|
||||
(if (empty? organizations)
|
||||
(rx/of (dt/teams-fetched teams)
|
||||
(modal/show :no-permission-modal {:type :no-orgs-create}))
|
||||
(->> (fetch-orgs-allowed team-id orgs)
|
||||
(modal/show :no-permission-modal {:type :no-organizations-create}))
|
||||
(->> (fetch-organizations-allowed team-id organizations)
|
||||
(rx/mapcat
|
||||
(fn [orgs-allowed]
|
||||
(let [valid-orgs (filterv #(true? (get orgs-allowed (:id %))) orgs)]
|
||||
(fn [organizations-allowed]
|
||||
(let [valid-organizations (filterv #(true? (get organizations-allowed (:id %))) organizations)]
|
||||
(rx/of
|
||||
(dt/teams-fetched teams)
|
||||
(if (empty? valid-orgs)
|
||||
(if (empty? valid-organizations)
|
||||
(modal/show
|
||||
{:type :alert
|
||||
:hide-actions? true
|
||||
:message (tr "dashboard.team-organization.add.no-valid-orgs")
|
||||
:title (tr "dashboard.select-org-modal.title")})
|
||||
(show-select-modal orgs-allowed))))))))))))))))
|
||||
:message (tr "dashboard.team-organization.add.no-valid-organizations")
|
||||
:title (tr "dashboard.select-organization-modal.title")})
|
||||
(show-select-modal organizations-allowed))))))))))))))))
|
||||
|
||||
(defn show-change-team-org-modal
|
||||
"Fetches fresh team/org data, then shows the change-org modal
|
||||
restricted to orgs where the user has permission, or the no-permission
|
||||
(defn show-change-team-organization-modal
|
||||
"Fetches fresh team/organization data, then shows the change-organization modal
|
||||
restricted to organizations where the user has permission, or the no-permission
|
||||
modal if none qualify."
|
||||
[{:keys [team-id]}]
|
||||
(ptk/reify ::show-change-team-org-modal
|
||||
(ptk/reify ::show-change-team-organization-modal
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [profile-id (dm/get-in state [:profile :id])]
|
||||
(->> (rp/cmd! :get-teams)
|
||||
(rx/mapcat
|
||||
(fn [teams]
|
||||
(let [all-orgs (map dt/team->organization
|
||||
(filter #(and (:is-default %) (:organization %)) teams))
|
||||
(let [all-organizations (map dt/team->organization
|
||||
(filter #(and (:is-default %) (:organization %)) teams))
|
||||
team (first (filter #(= (:id %) team-id) teams))
|
||||
source-org (:organization team)
|
||||
current-org-id (:id source-org)
|
||||
move-perm (dm/get-in source-org [:permissions :move-teams])
|
||||
source-owner-id (:owner-id source-org)
|
||||
can-create? (fn [org]
|
||||
(let [perm (dm/get-in org [:permissions :create-teams])
|
||||
is-own? (= profile-id (:owner-id org))]
|
||||
source-organization (:organization team)
|
||||
current-organization-id (:id source-organization)
|
||||
move-perm (dm/get-in source-organization [:permissions :move-teams])
|
||||
source-owner-id (:owner-id source-organization)
|
||||
can-create? (fn [organization]
|
||||
(let [perm (dm/get-in organization [:permissions :create-teams])
|
||||
is-own? (= profile-id (:owner-id organization))]
|
||||
(or (= perm "any") is-own?)))
|
||||
orgs-by-move (case move-perm
|
||||
"never"
|
||||
[]
|
||||
organizations-by-move (case move-perm
|
||||
"never"
|
||||
[]
|
||||
|
||||
"myOrganizations"
|
||||
(filter #(= source-owner-id (:owner-id %)) all-orgs)
|
||||
"myOrganizations"
|
||||
(filter #(= source-owner-id (:owner-id %)) all-organizations)
|
||||
|
||||
;; Default to always-allowed behavior.
|
||||
all-orgs)
|
||||
orgs (filter can-create? orgs-by-move)
|
||||
selectable-orgs (remove #(= current-org-id (:id %)) orgs)
|
||||
;; Default to always-allowed behavior.
|
||||
all-organizations)
|
||||
organizations (filter can-create? organizations-by-move)
|
||||
selectable-organizations (remove #(= current-organization-id (:id %)) organizations)
|
||||
on-confirm (fn [organization-id]
|
||||
(st/emit! (add-team-to-organization {:team-id team-id
|
||||
:organization-id organization-id})))]
|
||||
(if (empty? selectable-orgs)
|
||||
(if (empty? selectable-organizations)
|
||||
(rx/of (dt/teams-fetched teams)
|
||||
(modal/show :no-permission-modal {:type :no-orgs-change}))
|
||||
(->> (fetch-orgs-allowed team-id selectable-orgs)
|
||||
(modal/show :no-permission-modal {:type :no-organizations-change}))
|
||||
(->> (fetch-organizations-allowed team-id selectable-organizations)
|
||||
(rx/mapcat
|
||||
(fn [orgs-allowed]
|
||||
(let [valid-orgs (filterv #(true? (get orgs-allowed (:id %))) selectable-orgs)
|
||||
has-filtered? (< (count orgs) (count all-orgs))
|
||||
(fn [organizations-allowed]
|
||||
(let [valid-organizations (filterv #(true? (get organizations-allowed (:id %))) selectable-organizations)
|
||||
has-filtered? (< (count organizations) (count all-organizations))
|
||||
extra-props (when has-filtered?
|
||||
{:info-message-key "dashboard.select-org-modal.permission-info"})]
|
||||
{:info-message-key "dashboard.select-organization-modal.permission-info"})]
|
||||
(rx/of
|
||||
(dt/teams-fetched teams)
|
||||
(if (empty? valid-orgs)
|
||||
(if (empty? valid-organizations)
|
||||
(modal/show
|
||||
{:type :alert
|
||||
:hide-actions? true
|
||||
:message (tr "dashboard.team-organization.add.no-valid-orgs")
|
||||
:title (tr "dashboard.change-org-modal.title")})
|
||||
:message (tr "dashboard.team-organization.add.no-valid-organizations")
|
||||
:title (tr "dashboard.change-organization-modal.title")})
|
||||
(modal/show :select-organization-modal
|
||||
(merge {:organizations selectable-orgs
|
||||
:orgs-allowed orgs-allowed
|
||||
:current-organization-id current-org-id
|
||||
(merge {:organizations selectable-organizations
|
||||
:organizations-allowed organizations-allowed
|
||||
:current-organization-id current-organization-id
|
||||
:on-confirm on-confirm
|
||||
:team-id team-id
|
||||
:title-key "dashboard.change-org-modal.title"
|
||||
:choose-key "dashboard.change-org-modal.choose"
|
||||
:placeholder-key "dashboard.change-org-modal.select"
|
||||
:accept-key "dashboard.change-org-modal.accept"
|
||||
:title-key "dashboard.change-organization-modal.title"
|
||||
:choose-key "dashboard.change-organization-modal.choose"
|
||||
:placeholder-key "dashboard.change-organization-modal.select"
|
||||
:accept-key "dashboard.change-organization-modal.accept"
|
||||
:cancel-key "labels.cancel"}
|
||||
extra-props)))))))))))))))))
|
||||
|
||||
@ -93,7 +93,7 @@
|
||||
state))
|
||||
|
||||
(defn with-refreshed-team
|
||||
"Fetches fresh team data from the server to ensure up-to-date org
|
||||
"Fetches fresh team data from the server to ensure up-to-date organization
|
||||
permissions, updates the app state, and calls f with the fresh team data.
|
||||
Returns an observable of events."
|
||||
[team-id f]
|
||||
@ -106,7 +106,7 @@
|
||||
(f team)))))))
|
||||
|
||||
(defn check-and-create-team
|
||||
"Fetches fresh team data from the server to ensure up-to-date org
|
||||
"Fetches fresh team data from the server to ensure up-to-date organization
|
||||
permissions, then shows the team-form modal or a no-permission modal."
|
||||
[team-id]
|
||||
(ptk/reify ::check-and-create-team
|
||||
@ -116,23 +116,23 @@
|
||||
(with-refreshed-team team-id
|
||||
(fn [team]
|
||||
(let [organization (:organization team)
|
||||
in-org? (and (contains? cf/flags :nitrate) organization)
|
||||
can-create? (if in-org?
|
||||
in-organization? (and (contains? cf/flags :nitrate) organization)
|
||||
can-create? (if in-organization?
|
||||
(nitrate-perms/allowed? :create-team
|
||||
{:org-perms {:owner-id (:owner-id organization)
|
||||
:permissions (:permissions organization)}
|
||||
{:organization-perms {:owner-id (:owner-id organization)
|
||||
:permissions (:permissions organization)}
|
||||
:profile-id profile-id
|
||||
:team-perms (:permissions team)})
|
||||
true)]
|
||||
(rx/of (if can-create?
|
||||
(modal/show :team-form (if in-org?
|
||||
(modal/show :team-form (if in-organization?
|
||||
{:organization-id (:id organization)
|
||||
:organization-name (:name organization)}
|
||||
{}))
|
||||
(modal/show :no-permission-modal {:type :create-team}))))))))))
|
||||
|
||||
(defn check-and-delete-team
|
||||
"Fetches fresh team data from the server to ensure up-to-date org
|
||||
"Fetches fresh team data from the server to ensure up-to-date organization
|
||||
permissions, then shows the confirmation modal or a no-permission modal."
|
||||
[{:keys [team-id delete-fn]}]
|
||||
(ptk/reify ::check-and-delete-team
|
||||
@ -141,17 +141,17 @@
|
||||
(let [profile-id (dm/get-in state [:profile :id])]
|
||||
(with-refreshed-team team-id
|
||||
(fn [team]
|
||||
(let [org (:organization team)
|
||||
in-org? (and (contains? cf/flags :nitrate) org)
|
||||
can-delete? (if in-org?
|
||||
(let [organization (:organization team)
|
||||
in-organization? (and (contains? cf/flags :nitrate) organization)
|
||||
can-delete? (if in-organization?
|
||||
(nitrate-perms/allowed? :delete-team
|
||||
{:org-perms {:owner-id (:owner-id org)
|
||||
:permissions (:permissions org)}
|
||||
{:organization-perms {:owner-id (:owner-id organization)
|
||||
:permissions (:permissions organization)}
|
||||
:profile-id profile-id
|
||||
:team-perms (:permissions team)})
|
||||
(boolean (dm/get-in team [:permissions :is-owner])))
|
||||
message (if in-org?
|
||||
(tr "modals.delete-org-team-confirm.message" (:name org))
|
||||
message (if in-organization?
|
||||
(tr "modals.delete-organization-team-confirm.message" (:name organization))
|
||||
(tr "modals.delete-team-confirm.message"))]
|
||||
(rx/of (if can-delete?
|
||||
(modal/show
|
||||
@ -163,7 +163,7 @@
|
||||
(modal/show :no-permission-modal {:type :delete-team}))))))))))
|
||||
|
||||
(defn- check-new-team-members-permission-and-show-invite-members
|
||||
"Receives refreshed team data with up-to-date org
|
||||
"Receives refreshed team data with up-to-date organization
|
||||
permissions, then shows the invite members modal or an appropriate alert."
|
||||
[{:keys [team invite-email origin]}]
|
||||
(ptk/reify ::check-new-team-members-permission-and-show-invite-members
|
||||
@ -175,16 +175,16 @@
|
||||
:invite-email invite-email}))]
|
||||
(if (and (contains? cf/flags :nitrate)
|
||||
(not (nitrate-perms/allowed? :add-anybody-to-team
|
||||
{:org-perms (:organization team)})))
|
||||
(->> (rp/cmd! :all-org-members-in-team
|
||||
{:organization-perms (:organization team)})))
|
||||
(->> (rp/cmd! :all-organization-members-in-team
|
||||
{:team-id (:id team)
|
||||
:organization-id (get-in team [:organization :id])})
|
||||
(rx/mapcat
|
||||
(fn [all-org-members-in-team?]
|
||||
(if all-org-members-in-team?
|
||||
(fn [all-organization-members-in-team?]
|
||||
(if all-organization-members-in-team?
|
||||
(rx/of (modal/show
|
||||
{:type :alert
|
||||
:message (tr "modals.invite-restricted-members.all-org-members-in-team" (get-in team [:organization :name]))
|
||||
:message (tr "modals.invite-restricted-members.all-organization-members-in-team" (get-in team [:organization :name]))
|
||||
:accept-label (tr "labels.accept")
|
||||
:accept-style :primary
|
||||
:title (tr "modals.invite-team-member.title")}))
|
||||
@ -192,7 +192,7 @@
|
||||
show-invite)))))
|
||||
|
||||
(defn check-and-invite-members
|
||||
"Fetches fresh team data from the server to ensure up-to-date org
|
||||
"Fetches fresh team data from the server to ensure up-to-date organization
|
||||
permissions, then shows invite-members modal or a permission error."
|
||||
[{:keys [team-id origin invite-email]
|
||||
:or {origin :team}}]
|
||||
@ -202,10 +202,10 @@
|
||||
(let [profile-id (dm/get-in state [:profile :id])]
|
||||
(with-refreshed-team team-id
|
||||
(fn [team]
|
||||
(let [org (:organization team)
|
||||
(let [organization (:organization team)
|
||||
can-invite? (nitrate-perms/can-send-invitations?
|
||||
{:nitrate-enabled? (contains? cf/flags :nitrate)
|
||||
:organization org
|
||||
:organization organization
|
||||
:profile-id profile-id
|
||||
:team-permissions (:permissions team)})]
|
||||
(rx/of (if can-invite?
|
||||
@ -609,7 +609,7 @@
|
||||
(rx/catch on-error))))))
|
||||
|
||||
(defn check-and-submit-invite-members
|
||||
"Fetches fresh team data from the server to ensure up-to-date org
|
||||
"Fetches fresh team data from the server to ensure up-to-date organization
|
||||
permissions, then submits member invitations or shows a restriction modal."
|
||||
[{:keys [team-id] :as params} origin do-invite-members]
|
||||
(ptk/reify ::check-and-submit-invite-members
|
||||
@ -619,9 +619,9 @@
|
||||
(with-refreshed-team team-id
|
||||
(fn [team]
|
||||
(if (not (nitrate-perms/allowed? :add-anybody-to-team
|
||||
{:org-perms (:organization team)}))
|
||||
(->> (rp/cmd! :check-org-members {:organization-id (get-in team [:organization :id])
|
||||
:emails (vec (:emails params))})
|
||||
{:organization-perms (:organization team)}))
|
||||
(->> (rp/cmd! :check-organization-members {:organization-id (get-in team [:organization :id])
|
||||
:emails (vec (:emails params))})
|
||||
(rx/mapcat
|
||||
(fn [result]
|
||||
(let [blocked (into [] (comp (filter (fn [[_ v]] (not v)))
|
||||
@ -830,5 +830,5 @@
|
||||
|
||||
|
||||
(defn team->organization [team]
|
||||
(when-let [org (:organization team)]
|
||||
(assoc org :default-team-id (:id team))))
|
||||
(when-let [organization (:organization team)]
|
||||
(assoc organization :default-team-id (:id team))))
|
||||
|
||||
@ -137,7 +137,7 @@
|
||||
:notification (dc/handle-notification msg)
|
||||
:team-role-change (handle-change-team-role msg)
|
||||
:team-membership-change (dc/team-membership-change msg)
|
||||
:team-org-change (dc/handle-change-team-org msg)
|
||||
:team-organization-change (dc/handle-change-team-organization msg)
|
||||
:organization-change-sso (dc/handle-organization-change-sso msg)
|
||||
nil))
|
||||
|
||||
|
||||
@ -44,7 +44,7 @@
|
||||
(st/emit! (da/login-from-token tdata)))
|
||||
|
||||
(defmethod handle-token :team-invitation
|
||||
[{:keys [state team-id org-team-id organization-name invitation-token] :as tdata}]
|
||||
[{:keys [state team-id organization-team-id organization-name invitation-token] :as tdata}]
|
||||
(when-let [{:keys [origin props]} (:organization-invitation-audit tdata)]
|
||||
(st/emit!
|
||||
(ev/event
|
||||
@ -54,11 +54,11 @@
|
||||
|
||||
(case state
|
||||
:created
|
||||
(if org-team-id
|
||||
(if organization-team-id
|
||||
(st/emit!
|
||||
(du/refresh-profile)
|
||||
(dcm/go-to-dashboard-recent :team-id org-team-id)
|
||||
(ntf/success (tr "auth.notifications.org-invitation-accepted" organization-name)))
|
||||
(dcm/go-to-dashboard-recent :team-id organization-team-id)
|
||||
(ntf/success (tr "auth.notifications.organization-invitation-accepted" organization-name)))
|
||||
(st/emit!
|
||||
(du/refresh-profile)
|
||||
(dcm/go-to-dashboard-recent :team-id team-id)
|
||||
@ -99,10 +99,10 @@
|
||||
(st/emit!
|
||||
(rt/nav :dashboard-recent {:team-id team-id}))
|
||||
|
||||
(= :org-not-found code)
|
||||
(= :organization-not-found code)
|
||||
(st/emit!
|
||||
(rt/nav :dashboard-recent {:team-id team-id})
|
||||
(ntf/error (tr "errors.org-not-found")))
|
||||
(ntf/error (tr "errors.organization-not-found")))
|
||||
|
||||
(= :canceled-invitation code)
|
||||
(let [profile (:profile @st/state)
|
||||
|
||||
@ -1,41 +0,0 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.main.ui.components.org-avatar
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc org-avatar*
|
||||
[{:keys [org size]}]
|
||||
(let [name (:name org)
|
||||
custom-photo (:custom-photo org)
|
||||
avatar-bg (:avatar-bg-url org)
|
||||
initials (d/get-initials name)]
|
||||
|
||||
(if custom-photo
|
||||
[:img {:src custom-photo
|
||||
:class (stl/css-case :org-avatar true
|
||||
:org-avatar-custom true
|
||||
:org-avatar-xxxl (= size "xxxl")
|
||||
:org-avatar-xxl (= size "xxl")
|
||||
:org-avatar-xl (= size "xl"))
|
||||
:alt name}]
|
||||
[:div {:class (stl/css-case :org-avatar true
|
||||
:org-avatar-xxxl (= size "xxxl")
|
||||
:org-avatar-xxl (= size "xxl")
|
||||
:org-avatar-xl (= size "xl"))
|
||||
:aria-hidden "true"}
|
||||
[:img {:src avatar-bg
|
||||
:class (stl/css :org-avatar-bg)
|
||||
:alt ""}]
|
||||
(when (seq initials)
|
||||
[:span {:class (stl/css-case :org-avatar-initials true
|
||||
:size-initials-xxxl (= size "xxxl")
|
||||
:size-initials-xxl (= size "xxl")
|
||||
:size-initials-xxl (= size "xl"))} ;; Keep the initials as xxl to make them legible
|
||||
initials])])))
|
||||
41
frontend/src/app/main/ui/components/organization_avatar.cljs
Normal file
41
frontend/src/app/main/ui/components/organization_avatar.cljs
Normal file
@ -0,0 +1,41 @@
|
||||
;; 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.ui.components.organization-avatar
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc organization-avatar*
|
||||
[{:keys [organization size]}]
|
||||
(let [name (:name organization)
|
||||
custom-photo (:custom-photo organization)
|
||||
avatar-bg (:avatar-bg-url organization)
|
||||
initials (d/get-initials name)]
|
||||
|
||||
(if custom-photo
|
||||
[:img {:src custom-photo
|
||||
:class (stl/css-case :organization-avatar true
|
||||
:organization-avatar-custom true
|
||||
:organization-avatar-xxxl (= size "xxxl")
|
||||
:organization-avatar-xxl (= size "xxl")
|
||||
:organization-avatar-xl (= size "xl"))
|
||||
:alt name}]
|
||||
[:div {:class (stl/css-case :organization-avatar true
|
||||
:organization-avatar-xxxl (= size "xxxl")
|
||||
:organization-avatar-xxl (= size "xxl")
|
||||
:organization-avatar-xl (= size "xl"))
|
||||
:aria-hidden "true"}
|
||||
[:img {:src avatar-bg
|
||||
:class (stl/css :organization-avatar-bg)
|
||||
:alt ""}]
|
||||
(when (seq initials)
|
||||
[:span {:class (stl/css-case :organization-avatar-initials true
|
||||
:size-initials-xxxl (= size "xxxl")
|
||||
:size-initials-xxl (= size "xxl")
|
||||
:size-initials-xxl (= size "xl"))} ;; Keep the initials as xxl to make them legible
|
||||
initials])])))
|
||||
@ -8,35 +8,35 @@
|
||||
@use "ds/colors.scss" as *;
|
||||
@use "ds/_utils.scss" as *;
|
||||
|
||||
.org-avatar {
|
||||
.organization-avatar {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.org-avatar-custom {
|
||||
.organization-avatar-custom {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.org-avatar-xxxl {
|
||||
.organization-avatar-xxxl {
|
||||
width: px2rem(30);
|
||||
height: px2rem(30);
|
||||
border-radius: px2rem(6);
|
||||
}
|
||||
|
||||
.org-avatar-xxl {
|
||||
.organization-avatar-xxl {
|
||||
width: var(--sp-xxl);
|
||||
height: var(--sp-xxl);
|
||||
border-radius: px2rem(5);
|
||||
}
|
||||
|
||||
.org-avatar-xl {
|
||||
.organization-avatar-xl {
|
||||
width: px2rem(22);
|
||||
height: px2rem(22);
|
||||
border-radius: var(--sp-xs);
|
||||
}
|
||||
|
||||
.org-avatar-bg {
|
||||
.organization-avatar-bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
@ -44,7 +44,7 @@
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.org-avatar-initials {
|
||||
.organization-avatar-initials {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@ -97,20 +97,20 @@
|
||||
:form form
|
||||
:default default-member-id}])]))
|
||||
|
||||
(defn- make-leave-org-modal-form-schema [teams]
|
||||
(defn- make-leave-organization-modal-form-schema [teams]
|
||||
(into
|
||||
[:map {:title "LeaveOrgModalForm"}]
|
||||
(for [team teams]
|
||||
[(keyword (str "member-id-" (:id team))) ::sm/text])))
|
||||
|
||||
|
||||
(mf/defc leave-and-reassign-org-modal
|
||||
(mf/defc leave-and-reassign-organization-modal
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :leave-and-reassign-org
|
||||
::mf/register-as :leave-and-reassign-organization
|
||||
::mf/wrap [mf/memo]}
|
||||
[{:keys [profile teams-to-transfer num-teams-to-delete accept] :as props}]
|
||||
(let [schema (mf/with-memo [teams-to-transfer]
|
||||
(make-leave-org-modal-form-schema teams-to-transfer))
|
||||
(make-leave-organization-modal-form-schema teams-to-transfer))
|
||||
;; Compute initial values for each team select
|
||||
team-fields (mf/with-memo [teams-to-transfer]
|
||||
(for [team teams-to-transfer]
|
||||
@ -146,21 +146,21 @@
|
||||
team-fields)]
|
||||
(accept {:teams-to-transfer teams-to-transfer})))]
|
||||
[:div {:class (stl/css :modal-overlay)}
|
||||
[:div {:class (stl/css :modal-org-container)}
|
||||
[:div {:class (stl/css :modal-organization-container)}
|
||||
[:div {:class (stl/css :modal-header)}
|
||||
[:h2 {:class (stl/css :modal-org-title)} (tr "modals.before-leave-org.title")]
|
||||
[:h2 {:class (stl/css :modal-organization-title)} (tr "modals.before-leave-organization.title")]
|
||||
[:button {:class (stl/css :modal-close-btn)
|
||||
:on-click modal/hide!} deprecated-icon/close]]
|
||||
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
(if (zero? num-teams-to-delete)
|
||||
[:p {:class (stl/css :modal-org-msg)}
|
||||
(tr "modals.leave-org-and-reassign.hint")]
|
||||
[:p {:class (stl/css :modal-organization-msg)}
|
||||
(tr "modals.leave-organization-and-reassign.hint")]
|
||||
[:*
|
||||
[:p {:class (stl/css :modal-org-msg)}
|
||||
(tr "modals.leave-org-and-reassign.hint-delete")]
|
||||
[:p {:class (stl/css :modal-org-msg)}
|
||||
(tr "modals.leave-org-and-reassign.hint-promote")]])
|
||||
[:p {:class (stl/css :modal-organization-msg)}
|
||||
(tr "modals.leave-organization-and-reassign.hint-delete")]
|
||||
[:p {:class (stl/css :modal-organization-msg)}
|
||||
(tr "modals.leave-organization-and-reassign.hint-promote")]])
|
||||
[:& fm/form {:form form}
|
||||
[:div {:class (stl/css :teams-container)}
|
||||
(for [{:keys [team field-name default-member-id]} team-fields]
|
||||
|
||||
@ -78,20 +78,20 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.modal-org-container {
|
||||
.modal-organization-container {
|
||||
@extend %modal-container-base;
|
||||
|
||||
overflow-y: auto;
|
||||
max-height: $sz-512;
|
||||
}
|
||||
|
||||
.modal-org-title {
|
||||
.modal-organization-title {
|
||||
@include t.use-typography("headline-large");
|
||||
|
||||
color: var(--modal-title-foreground-color);
|
||||
}
|
||||
|
||||
.modal-org-msg {
|
||||
.modal-organization-msg {
|
||||
@include t.use-typography("body-large");
|
||||
|
||||
color: var(--modal-text-foreground-color);
|
||||
|
||||
@ -25,7 +25,7 @@
|
||||
[app.main.ui.components.dropdown-menu :refer [dropdown-menu*
|
||||
dropdown-menu-item*]]
|
||||
[app.main.ui.components.link :refer [link*]]
|
||||
[app.main.ui.components.org-avatar :refer [org-avatar*]]
|
||||
[app.main.ui.components.organization-avatar :refer [organization-avatar*]]
|
||||
[app.main.ui.dashboard.comments :refer [comments-icon* comments-section]]
|
||||
[app.main.ui.dashboard.inline-edition :refer [inline-edition]]
|
||||
[app.main.ui.dashboard.project-menu :refer [project-menu*]]
|
||||
@ -74,11 +74,11 @@
|
||||
(def ^:private menu-icon
|
||||
(deprecated-icon/icon-xref :menu (stl/css :menu-icon)))
|
||||
|
||||
(def ^:private org-menu-icon
|
||||
(deprecated-icon/icon-xref :menu (stl/css :org-menu-icon)))
|
||||
(def ^:private organization-menu-icon
|
||||
(deprecated-icon/icon-xref :menu (stl/css :organization-menu-icon)))
|
||||
|
||||
(def ^:private org-menu-icon-open
|
||||
(deprecated-icon/icon-xref :menu (stl/css :org-menu-icon-open)))
|
||||
(def ^:private organization-menu-icon-open
|
||||
(deprecated-icon/icon-xref :menu (stl/css :organization-menu-icon-open)))
|
||||
|
||||
(def ^:private pin-icon
|
||||
(deprecated-icon/icon-xref :pin (stl/css :pin-icon)))
|
||||
@ -86,8 +86,8 @@
|
||||
(def ^:private exit-icon
|
||||
(deprecated-icon/icon-xref :exit (stl/css :exit-icon)))
|
||||
|
||||
(def ^:private add-org-icon
|
||||
(deprecated-icon/icon-xref :add (stl/css :add-org-icon)))
|
||||
(def ^:private add-organization-icon
|
||||
(deprecated-icon/icon-xref :add (stl/css :add-organization-icon)))
|
||||
|
||||
(def ^:private arrow-up-right-icon
|
||||
(deprecated-icon/icon-xref :arrow-up-right (stl/css :arrow-up-right-icon)))
|
||||
@ -312,7 +312,7 @@
|
||||
|
||||
account-age-days (dnt/account-age-days profile)
|
||||
|
||||
on-org-click
|
||||
on-organization-click
|
||||
(mf/use-fn
|
||||
(fn [event]
|
||||
(let [team-id (-> (dom/get-current-target event)
|
||||
@ -320,7 +320,7 @@
|
||||
(uuid/parse))]
|
||||
(st/emit! (dcm/go-to-dashboard-recent :team-id team-id)))))
|
||||
|
||||
on-create-org-click
|
||||
on-create-organization-click
|
||||
(mf/use-fn
|
||||
(mf/deps account-age-days profile subscription-type team-count)
|
||||
(fn []
|
||||
@ -348,15 +348,15 @@
|
||||
(mf/use-fn
|
||||
(mf/deps organization profile)
|
||||
(fn []
|
||||
;; Navigate to active org if user owns it, otherwise to last visited org
|
||||
;; Navigate to active organization if user owns it, otherwise to last visited organization
|
||||
(if (and (:id organization)
|
||||
(= (:id profile) (:owner-id organization)))
|
||||
(dnt/go-to-nitrate-ac {:organization-id (:id organization)
|
||||
:organization-slug (:slug organization)})
|
||||
(dnt/go-to-nitrate-ac))))
|
||||
|
||||
empty-org (d/seek #(nil? (:id %)) organizations)
|
||||
default-team-id (or (:default-team-id empty-org)
|
||||
empty-organization (d/seek #(nil? (:id %)) organizations)
|
||||
default-team-id (or (:default-team-id empty-organization)
|
||||
(:default-team-id profile))
|
||||
|
||||
organizations (filter :id organizations)
|
||||
@ -365,9 +365,9 @@
|
||||
|
||||
[:> dropdown-menu* props
|
||||
|
||||
[:> dropdown-menu-item* {:on-click on-org-click
|
||||
[:> dropdown-menu-item* {:on-click on-organization-click
|
||||
:data-value default-team-id
|
||||
:class (stl/css :org-dropdown-item)}
|
||||
:class (stl/css :organization-dropdown-item)}
|
||||
[:span {:class (stl/css :my-teams-icon)}
|
||||
[:> raw-svg* {:id penpot-logo-icon-subtle}]]
|
||||
[:span {:class (stl/css :team-text)
|
||||
@ -378,28 +378,28 @@
|
||||
(when (seq organizations)
|
||||
[:*
|
||||
[:hr {:role "separator" :class (stl/css :team-separator)}]
|
||||
[:li {:role "presentation" :class (stl/css :org-section-label)}
|
||||
[:li {:role "presentation" :class (stl/css :organization-section-label)}
|
||||
(tr "dashboard.section.organizations")]])
|
||||
|
||||
(for [org-item organizations]
|
||||
[:> dropdown-menu-item* {:on-click on-org-click
|
||||
:data-value (:default-team-id org-item)
|
||||
:class (stl/css :org-dropdown-item)
|
||||
:key (str (:default-team-id org-item))}
|
||||
[:> org-avatar* {:org org-item :size "xxl"}]
|
||||
(for [organization-item organizations]
|
||||
[:> dropdown-menu-item* {:on-click on-organization-click
|
||||
:data-value (:default-team-id organization-item)
|
||||
:class (stl/css :organization-dropdown-item)
|
||||
:key (str (:default-team-id organization-item))}
|
||||
[:> organization-avatar* {:organization organization-item :size "xxl"}]
|
||||
[:span {:class (stl/css :team-text)
|
||||
:title (:name org-item)} (:name org-item)]
|
||||
(when (= (:default-team-id org-item) (:default-team-id organization))
|
||||
:title (:name organization-item)} (:name organization-item)]
|
||||
(when (= (:default-team-id organization-item) (:default-team-id organization))
|
||||
tick-icon)])
|
||||
|
||||
[:hr {:role "separator" :class (stl/css :team-separator)}]
|
||||
[:> dropdown-menu-item* {:on-click on-create-org-click
|
||||
:class (stl/css :org-dropdown-item :action)}
|
||||
[:span {:class (stl/css :icon-wrapper)} add-org-icon]
|
||||
[:span {:class (stl/css :team-text)} (tr "dashboard.create-new-org")]]
|
||||
[:> dropdown-menu-item* {:on-click on-create-organization-click
|
||||
:class (stl/css :organization-dropdown-item :action)}
|
||||
[:span {:class (stl/css :icon-wrapper)} add-organization-icon]
|
||||
[:span {:class (stl/css :team-text)} (tr "dashboard.create-new-organization")]]
|
||||
(when is-valid-license?
|
||||
[:> dropdown-menu-item* {:on-click on-go-to-cc-click
|
||||
:class (stl/css :org-dropdown-item :action)}
|
||||
:class (stl/css :organization-dropdown-item :action)}
|
||||
[:span {:class (stl/css :icon-wrapper)} arrow-up-right-icon]
|
||||
[:span {:class (stl/css :team-text)} (tr "dashboard.go-to-admin-console")]])]))
|
||||
|
||||
@ -446,7 +446,7 @@
|
||||
(seq (remove :is-default (vals teams))))
|
||||
[:*
|
||||
[:hr {:role "separator" :class (stl/css :team-separator)}]
|
||||
[:li {:role "presentation" :class (stl/css :org-section-label)}
|
||||
[:li {:role "presentation" :class (stl/css :organization-section-label)}
|
||||
(tr "dashboard.section.teams")]])
|
||||
|
||||
(for [team-item (remove :is-default (vals teams))]
|
||||
@ -624,7 +624,7 @@
|
||||
:data-testid "delete-team"}
|
||||
(tr "dashboard.delete-team")])]))
|
||||
|
||||
(mf/defc org-options-dropdown*
|
||||
(mf/defc organization-options-dropdown*
|
||||
{::mf/private true}
|
||||
[{:keys [organization profile teams] :rest props}]
|
||||
(let [default-team-id (mf/with-memo [teams]
|
||||
@ -649,8 +649,8 @@
|
||||
(fn [error]
|
||||
(let [code (-> error ex-data :code)
|
||||
;; Map error codes to their translation keys
|
||||
error-map {:not-valid-teams "errors.org-leave.no-valid-teams"
|
||||
:org-owner-cannot-leave "errors.org-leave.org-owner-cannot-leave"
|
||||
error-map {:not-valid-teams "errors.organization-leave.no-valid-teams"
|
||||
:organization-owner-cannot-leave "errors.organization-leave.organization-owner-cannot-leave"
|
||||
:only-owner-can-delete-team "errors.team-leave.only-owner-can-delete"
|
||||
:no-enough-members-for-leave "errors.team-leave.insufficient-members"
|
||||
:member-does-not-exist "errors.team-leave.member-does-not-exists"
|
||||
@ -678,27 +678,27 @@
|
||||
(map :id))]
|
||||
|
||||
|
||||
(st/emit! (dnt/leave-org {:id (:id organization)
|
||||
:name (:name organization)
|
||||
:default-team-id default-team-id
|
||||
:teams-to-delete teams-to-delete
|
||||
:teams-to-leave teams-to-leave
|
||||
:member-added-at member-added-at
|
||||
:organization-member-count-before
|
||||
organization-member-count-before
|
||||
:on-error on-error})))))
|
||||
(st/emit! (dnt/leave-organization {:id (:id organization)
|
||||
:name (:name organization)
|
||||
:default-team-id default-team-id
|
||||
:teams-to-delete teams-to-delete
|
||||
:teams-to-leave teams-to-leave
|
||||
:member-added-at member-added-at
|
||||
:organization-member-count-before
|
||||
organization-member-count-before
|
||||
:on-error on-error})))))
|
||||
|
||||
on-leave-clicked
|
||||
(mf/use-fn
|
||||
(mf/deps leave-fn profile organization default-team-id teams-to-transfer on-error owned-teams-members-loaded?)
|
||||
(fn []
|
||||
(when owned-teams-members-loaded?
|
||||
(st/emit! (dnt/show-leave-org-modal {:organization organization
|
||||
:profile profile
|
||||
:default-team-id default-team-id
|
||||
:leave-fn leave-fn
|
||||
:teams-to-transfer teams-to-transfer
|
||||
:on-error on-error})))))]
|
||||
(st/emit! (dnt/show-leave-organization-modal {:organization organization
|
||||
:profile profile
|
||||
:default-team-id default-team-id
|
||||
:leave-fn leave-fn
|
||||
:teams-to-transfer teams-to-transfer
|
||||
:on-error on-error})))))]
|
||||
(mf/use-effect
|
||||
(mf/deps owned-teams)
|
||||
(fn []
|
||||
@ -710,10 +710,10 @@
|
||||
|
||||
[:> dropdown-menu-item* {:on-click on-leave-clicked
|
||||
:class (stl/css :team-options-item)}
|
||||
(tr "dashboard.leave-org")]]))
|
||||
(tr "dashboard.leave-organization")]]))
|
||||
|
||||
|
||||
(mf/defc sidebar-org-switch*
|
||||
(mf/defc sidebar-organization-switch*
|
||||
[{:keys [team profile]}]
|
||||
(let [teams (mf/deref refs/teams)
|
||||
|
||||
@ -723,70 +723,70 @@
|
||||
|
||||
account-age-days (dnt/account-age-days profile)
|
||||
|
||||
current-org (dtm/team->organization team)
|
||||
current-organization (dtm/team->organization team)
|
||||
|
||||
;; Find the "your-penpot" teams, and transform them in orgs. When
|
||||
;; Find the "your-penpot" teams, and transform them in organizations. When
|
||||
;; the selected team is directly accessible but not listed in
|
||||
;; membership teams, include only its org so the org selector can
|
||||
;; membership teams, include only its organization so the organization selector can
|
||||
;; show the current selection without leaking the team into the
|
||||
;; teams dropdown.
|
||||
orgs (mf/with-memo [teams current-org]
|
||||
(cond-> (->> teams
|
||||
vals
|
||||
(filter :is-default)
|
||||
(map dtm/team->organization)
|
||||
(d/index-by :id))
|
||||
(:id current-org)
|
||||
(assoc (:id current-org) current-org)))
|
||||
organizations (mf/with-memo [teams current-organization]
|
||||
(cond-> (->> teams
|
||||
vals
|
||||
(filter :is-default)
|
||||
(map dtm/team->organization)
|
||||
(d/index-by :id))
|
||||
(:id current-organization)
|
||||
(assoc (:id current-organization) current-organization)))
|
||||
|
||||
show-dropdown? (or (dnt/is-valid-license? profile)
|
||||
(> (count orgs) 1))
|
||||
(> (count organizations) 1))
|
||||
|
||||
org-teams (mf/with-memo [teams current-org]
|
||||
(->> teams
|
||||
vals
|
||||
(filter #(= (dm/get-in % [:organization :id]) (:id current-org)))))
|
||||
organization-teams (mf/with-memo [teams current-organization]
|
||||
(->> teams
|
||||
vals
|
||||
(filter #(= (dm/get-in % [:organization :id]) (:id current-organization)))))
|
||||
|
||||
default-org? (nil? (:id current-org))
|
||||
default-organization? (nil? (:id current-organization))
|
||||
|
||||
show-options? (and (not default-org?)
|
||||
(not= (:id profile) (:owner-id current-org)))
|
||||
show-options? (and (not default-organization?)
|
||||
(not= (:id profile) (:owner-id current-organization)))
|
||||
|
||||
show-orgs-menu*
|
||||
show-organizations-menu*
|
||||
(mf/use-state false)
|
||||
|
||||
show-orgs-menu?
|
||||
(deref show-orgs-menu*)
|
||||
show-organizations-menu?
|
||||
(deref show-organizations-menu*)
|
||||
|
||||
orgs-rect*
|
||||
organizations-rect*
|
||||
(mf/use-state nil)
|
||||
|
||||
orgs-portal-container
|
||||
organizations-portal-container
|
||||
(hooks/use-portal-container :popup)
|
||||
|
||||
show-org-options-menu*
|
||||
show-organization-options-menu*
|
||||
(mf/use-state false)
|
||||
|
||||
show-org-options-menu?
|
||||
(deref show-org-options-menu*)
|
||||
show-organization-options-menu?
|
||||
(deref show-organization-options-menu*)
|
||||
|
||||
on-show-options-click
|
||||
(mf/use-fn
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(swap! show-org-options-menu* not)))
|
||||
(swap! show-organization-options-menu* not)))
|
||||
|
||||
close-org-options-menu
|
||||
(mf/use-fn #(reset! show-org-options-menu* false))
|
||||
close-organization-options-menu
|
||||
(mf/use-fn #(reset! show-organization-options-menu* false))
|
||||
|
||||
on-show-orgs-click
|
||||
on-show-organizations-click
|
||||
(mf/use-fn
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(reset! orgs-rect* (dom/get-bounding-rect (dom/get-current-target event)))
|
||||
(swap! show-orgs-menu* not)))
|
||||
(reset! organizations-rect* (dom/get-bounding-rect (dom/get-current-target event)))
|
||||
(swap! show-organizations-menu* not)))
|
||||
|
||||
on-show-orgs-keydown
|
||||
on-show-organizations-keydown
|
||||
(mf/use-fn
|
||||
(fn [event]
|
||||
(when (or (kbd/space? event)
|
||||
@ -795,10 +795,10 @@
|
||||
(dom/stop-propagation event)
|
||||
(some-> (dom/get-current-target event)
|
||||
(dom/click)))))
|
||||
close-orgs-menu
|
||||
(mf/use-fn #(reset! show-orgs-menu* false))
|
||||
close-organizations-menu
|
||||
(mf/use-fn #(reset! show-organizations-menu* false))
|
||||
|
||||
on-create-org-click
|
||||
on-create-organization-click
|
||||
(mf/use-fn
|
||||
(mf/deps account-age-days profile subscription-type team-count)
|
||||
(fn []
|
||||
@ -822,36 +822,36 @@
|
||||
(= subscription-type "unlimited")
|
||||
(assoc :show-contact-sales-option true)))))))]
|
||||
(if show-dropdown?
|
||||
[:div {:class (stl/css :sidebar-org-switch)}
|
||||
[:div {:class (stl/css :org-switch-content)}
|
||||
[:button {:class (stl/css-case :current-org true :current-org-no-options (not show-options?))
|
||||
:on-click on-show-orgs-click
|
||||
:on-key-down on-show-orgs-keydown
|
||||
:aria-expanded show-orgs-menu?
|
||||
[:div {:class (stl/css :sidebar-organization-switch)}
|
||||
[:div {:class (stl/css :organization-switch-content)}
|
||||
[:button {:class (stl/css-case :current-organization true :current-organization-no-options (not show-options?))
|
||||
:on-click on-show-organizations-click
|
||||
:on-key-down on-show-organizations-keydown
|
||||
:aria-expanded show-organizations-menu?
|
||||
:aria-haspopup "menu"}
|
||||
[:div {:class (stl/css :team-name)}
|
||||
(if default-org?
|
||||
(if default-organization?
|
||||
[:*
|
||||
[:span {:class (stl/css :my-teams-icon-xxxl)}
|
||||
[:> raw-svg* {:id penpot-logo-icon-subtle}]]
|
||||
[:span {:class (stl/css :team-text)}
|
||||
(tr "dashboard.my-teams")]]
|
||||
[:*
|
||||
[:> org-avatar* {:org current-org :size "xxxl"}]
|
||||
[:> organization-avatar* {:organization current-organization :size "xxxl"}]
|
||||
[:span {:class (stl/css :team-text)}
|
||||
(:name current-org)]])]
|
||||
(:name current-organization)]])]
|
||||
arrow-icon]
|
||||
(when show-options?
|
||||
[:> button* {:variant "ghost"
|
||||
:type "button"
|
||||
:class (stl/css :org-options-btn)
|
||||
:class (stl/css :organization-options-btn)
|
||||
:on-click on-show-options-click}
|
||||
(if show-org-options-menu? org-menu-icon-open org-menu-icon)])]
|
||||
(if show-organization-options-menu? organization-menu-icon-open organization-menu-icon)])]
|
||||
|
||||
|
||||
;; Orgs Dropdown
|
||||
(when show-orgs-menu?
|
||||
(let [rect (deref orgs-rect*)]
|
||||
;; Organizations dropdown
|
||||
(when show-organizations-menu?
|
||||
(let [rect (deref organizations-rect*)]
|
||||
(mf/portal
|
||||
(mf/html
|
||||
[:div {:style {:position "fixed"
|
||||
@ -859,40 +859,40 @@
|
||||
:left (dm/str (:left rect) "px")
|
||||
:width (dm/str (:width rect) "px")}}
|
||||
[:> organizations-selector-dropdown* {:show true
|
||||
:on-close close-orgs-menu
|
||||
:on-close close-organizations-menu
|
||||
:id "organizations-list"
|
||||
:class (stl/css :dropdown :teams-dropdown)
|
||||
:organization current-org
|
||||
:organization current-organization
|
||||
:profile profile
|
||||
:organizations (->> (vals orgs)
|
||||
:organizations (->> (vals organizations)
|
||||
(sort-by (juxt (fn [o] (str/lower (:name o "")))
|
||||
:id)))}]])
|
||||
|
||||
|
||||
|
||||
orgs-portal-container)))
|
||||
;; Orgs options
|
||||
[:> org-options-dropdown* {:show show-org-options-menu?
|
||||
:on-close close-org-options-menu
|
||||
:id "team-options"
|
||||
:class (stl/css :dropdown :options-dropdown)
|
||||
:organization current-org
|
||||
:profile profile
|
||||
:teams org-teams}]]
|
||||
[:div {:class (stl/css :selected-org)}
|
||||
[:span {:class (stl/css :org-penpot-icon)}
|
||||
organizations-portal-container)))
|
||||
;; Organization options
|
||||
[:> organization-options-dropdown* {:show show-organization-options-menu?
|
||||
:on-close close-organization-options-menu
|
||||
:id "team-options"
|
||||
:class (stl/css :dropdown :options-dropdown)
|
||||
:organization current-organization
|
||||
:profile profile
|
||||
:teams organization-teams}]]
|
||||
[:div {:class (stl/css :selected-organization)}
|
||||
[:span {:class (stl/css :organization-penpot-icon)}
|
||||
[:> raw-svg* {:id penpot-logo-icon}]]
|
||||
"Penpot"
|
||||
[:> button* {:variant "ghost"
|
||||
:type "button"
|
||||
:class (stl/css :create-org)
|
||||
:on-click on-create-org-click} (tr "dashboard.plus-create-new-org")]])))
|
||||
:class (stl/css :create-organization)
|
||||
:on-click on-create-organization-click} (tr "dashboard.plus-create-new-organization")]])))
|
||||
|
||||
(mf/defc sidebar-team-switch*
|
||||
[{:keys [team profile]}]
|
||||
(let [nitrate? (contains? cf/flags :nitrate)
|
||||
org (:organization team)
|
||||
organization-id (when nitrate? (:id org))
|
||||
organization (:organization team)
|
||||
organization-id (when nitrate? (:id organization))
|
||||
teams (cond->> (mf/deref refs/teams)
|
||||
nitrate?
|
||||
(filter #(= (dm/get-in (val %) [:organization :id]) organization-id))
|
||||
@ -1111,8 +1111,8 @@
|
||||
[:*
|
||||
[:div {:class (stl/css :sidebar-content-wrapper)}
|
||||
(when nitrate?
|
||||
[:div {:class (stl/css :orgs-container)}
|
||||
[:> sidebar-org-switch* {:team team :profile profile}]])
|
||||
[:div {:class (stl/css :organizations-container)}
|
||||
[:> sidebar-organization-switch* {:team team :profile profile}]])
|
||||
[:div {:ref container
|
||||
:class (stl/css-case :sidebar-content true :sidebar-content-nitrate nitrate?)}
|
||||
[:> sidebar-team-switch* {:team team :profile profile}]
|
||||
|
||||
@ -196,7 +196,7 @@
|
||||
grid-template-columns: 1fr auto;
|
||||
}
|
||||
|
||||
.org-dropdown-item {
|
||||
.organization-dropdown-item {
|
||||
@extend %menu-item-base;
|
||||
|
||||
display: grid;
|
||||
@ -238,7 +238,7 @@
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.org-section-label {
|
||||
.organization-section-label {
|
||||
@include t.use-typography("headline-small");
|
||||
|
||||
color: var(--color-foreground-secondary);
|
||||
@ -559,7 +559,7 @@
|
||||
stroke: var(--icon-foreground);
|
||||
}
|
||||
|
||||
.add-org-icon {
|
||||
.add-organization-icon {
|
||||
@extend %button-icon;
|
||||
|
||||
width: var(--sp-l);
|
||||
@ -606,7 +606,7 @@
|
||||
color: var(--color-accent-tertiary);
|
||||
}
|
||||
|
||||
.orgs-container {
|
||||
.organizations-container {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
height: calc(2 * var(--sp-xxxl));
|
||||
@ -615,7 +615,7 @@
|
||||
padding: 0 var(--sp-l);
|
||||
}
|
||||
|
||||
.selected-org {
|
||||
.selected-organization {
|
||||
@include t.use-typography("body-medium");
|
||||
|
||||
color: var(--color-foreground-primary);
|
||||
@ -626,12 +626,12 @@
|
||||
gap: var(--sp-s);
|
||||
}
|
||||
|
||||
.create-org {
|
||||
.create-organization {
|
||||
margin-inline-start: auto;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.org-penpot-icon {
|
||||
.organization-penpot-icon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@ -647,7 +647,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.org-icon {
|
||||
.organization-icon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@ -685,12 +685,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-org-switch {
|
||||
.sidebar-organization-switch {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.current-org {
|
||||
.current-organization {
|
||||
@include deprecated.button-style;
|
||||
|
||||
text-transform: none;
|
||||
@ -703,15 +703,15 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.current-org-no-options {
|
||||
.current-organization-no-options {
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.current-org .arrow-icon {
|
||||
.current-organization .arrow-icon {
|
||||
margin-inline-end: var(--sp-xs);
|
||||
}
|
||||
|
||||
.org-options {
|
||||
.organization-options {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@ -720,7 +720,7 @@
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.org-switch-content {
|
||||
.organization-switch-content {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
align-items: center;
|
||||
@ -728,7 +728,7 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.org-options-btn {
|
||||
.organization-options-btn {
|
||||
--icon-stroke: var(--icon-foreground);
|
||||
|
||||
display: flex;
|
||||
@ -742,13 +742,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
.org-menu-icon {
|
||||
.organization-menu-icon {
|
||||
@extend %button-icon;
|
||||
|
||||
stroke: var(--icon-stroke);
|
||||
}
|
||||
|
||||
.org-menu-icon-open {
|
||||
.organization-menu-icon-open {
|
||||
@extend %button-icon;
|
||||
|
||||
stroke: var(--color-accent-primary);
|
||||
|
||||
@ -158,11 +158,11 @@
|
||||
|
||||
teams-loaded? (seq teams)
|
||||
|
||||
no-orgs-created? (mf/with-memo [teams]
|
||||
(and (seq teams)
|
||||
(->> teams
|
||||
vals
|
||||
(not-any? :organization))))
|
||||
no-organizations-created? (mf/with-memo [teams]
|
||||
(and (seq teams)
|
||||
(->> teams
|
||||
vals
|
||||
(not-any? :organization))))
|
||||
|
||||
handle-click
|
||||
(mf/use-fn
|
||||
@ -199,17 +199,17 @@
|
||||
|
||||
[:*
|
||||
;; TODO add translations for this texts when we have the definitive ones
|
||||
(if (and nitrate? teams-loaded? no-orgs-created? (not show-subscription-warning?))
|
||||
(if (and nitrate? teams-loaded? no-organizations-created? (not show-subscription-warning?))
|
||||
;; Banner for users with active nitrate license but no organizations created
|
||||
[:div {:class (stl/css :nitrate-banner :highlighted)}
|
||||
[:div {:class (stl/css :nitrate-content)}
|
||||
[:span {:class (stl/css :nitrate-title)} (tr "subscription.banner.see-enterprise")]]
|
||||
[:div {:class (stl/css :nitrate-content)}
|
||||
[:span {:class (stl/css :nitrate-info)} (tr "subscription.banner.create-org-info")]
|
||||
[:span {:class (stl/css :nitrate-info)} (tr "subscription.banner.create-organization-info")]
|
||||
[:> button* {:variant "primary"
|
||||
:type "button"
|
||||
:class (stl/css :nitrate-bottom-button)
|
||||
:on-click handle-go-to-cc} (tr "nitrate.activation-success.create-org")]]]
|
||||
:on-click handle-go-to-cc} (tr "nitrate.activation-success.create-organization")]]]
|
||||
|
||||
;; Banner for users without nitrate license
|
||||
(when (not nitrate?)
|
||||
|
||||
@ -24,7 +24,7 @@
|
||||
[app.main.ui.components.dropdown :refer [dropdown]]
|
||||
[app.main.ui.components.file-uploader :refer [file-uploader]]
|
||||
[app.main.ui.components.forms :as fm]
|
||||
[app.main.ui.components.org-avatar :refer [org-avatar*]]
|
||||
[app.main.ui.components.organization-avatar :refer [organization-avatar*]]
|
||||
[app.main.ui.dashboard.change-owner]
|
||||
[app.main.ui.dashboard.subscription :refer [members-cta*
|
||||
show-subscription-members-banner?
|
||||
@ -56,8 +56,8 @@
|
||||
(def ^:private menu-icon
|
||||
(deprecated-icon/icon-xref :menu (stl/css :menu-icon)))
|
||||
|
||||
(def ^:private org-menu-icon
|
||||
(deprecated-icon/icon-xref :menu (stl/css :org-menu-icon)))
|
||||
(def ^:private organization-menu-icon
|
||||
(deprecated-icon/icon-xref :menu (stl/css :organization-menu-icon)))
|
||||
|
||||
(def ^:private warning-icon
|
||||
(deprecated-icon/icon-xref :msg-warning (stl/css :warning-icon)))
|
||||
@ -888,41 +888,41 @@
|
||||
(def schema:organization-form [:map {:title "SelectOrgForm"}
|
||||
[:selected-id ::sm/uuid]])
|
||||
|
||||
(mf/defc render-org-combobox-avatar*
|
||||
(mf/defc render-organization-combobox-avatar*
|
||||
[{:keys [avatar]}]
|
||||
[:> org-avatar* {:org (:organization avatar)
|
||||
:size (:size avatar)}])
|
||||
[:> organization-avatar* {:organization (:organization avatar)
|
||||
:size (:size avatar)}])
|
||||
|
||||
(mf/defc select-organization-modal
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :select-organization-modal}
|
||||
[{:keys [organizations orgs-allowed current-organization-id on-confirm title-key text-key choose-key placeholder-key accept-key cancel-key info-message-key team-id]}]
|
||||
[{:keys [organizations organizations-allowed current-organization-id on-confirm title-key text-key choose-key placeholder-key accept-key cancel-key info-message-key team-id]}]
|
||||
(let [valid-organizations (mf/with-memo [organizations]
|
||||
(remove #(= (:id %) current-organization-id) organizations))
|
||||
options (mf/with-memo [valid-organizations orgs-allowed]
|
||||
options (mf/with-memo [valid-organizations organizations-allowed]
|
||||
(mapv (fn [organization]
|
||||
(let [org-id (:id organization)
|
||||
;; orgs-allowed is a map of org-id and a boolean indicating if it is allowed
|
||||
enabled? (or (nil? orgs-allowed)
|
||||
(true? (get orgs-allowed org-id)))]
|
||||
(cond-> {:id (str org-id)
|
||||
(let [organization-id (:id organization)
|
||||
;; organizations-allowed is a map of organization-id and a boolean indicating if it is allowed
|
||||
enabled? (or (nil? organizations-allowed)
|
||||
(true? (get organizations-allowed organization-id)))]
|
||||
(cond-> {:id (str organization-id)
|
||||
:label (:name organization)
|
||||
:disabled (not enabled?)
|
||||
:dimmed (not enabled?)
|
||||
:avatar {:render-fn render-org-combobox-avatar*
|
||||
:avatar {:render-fn render-organization-combobox-avatar*
|
||||
:organization organization
|
||||
:size "xl"}}
|
||||
(not enabled?)
|
||||
(assoc :title (tr "dashboard.team-organization.disabled-org-tooltip")))))
|
||||
(assoc :title (tr "dashboard.team-organization.disabled-organization-tooltip")))))
|
||||
valid-organizations))
|
||||
|
||||
form (fm/use-form :schema schema:organization-form :initial {})
|
||||
|
||||
warning-info* (mf/use-state nil)
|
||||
warning-info (deref warning-info*)
|
||||
selected-org (mf/with-memo [warning-info valid-organizations]
|
||||
(when warning-info
|
||||
(d/seek #(= (:id %) (:organization-id warning-info)) valid-organizations)))
|
||||
selected-organization (mf/with-memo [warning-info valid-organizations]
|
||||
(when warning-info
|
||||
(d/seek #(= (:id %) (:organization-id warning-info)) valid-organizations)))
|
||||
|
||||
on-change
|
||||
(mf/use-fn
|
||||
@ -931,13 +931,13 @@
|
||||
(uforms/on-input-change form :selected-id id)
|
||||
;; Check for external invitations when selection changes
|
||||
(when (and team-id id)
|
||||
(let [org-id (d/parse-uuid id)]
|
||||
(let [organization-id (d/parse-uuid id)]
|
||||
(->> (rp/cmd! :check-team-external-invitations
|
||||
{:team-id team-id
|
||||
:organization-id org-id})
|
||||
:organization-id organization-id})
|
||||
(rx/subs!
|
||||
(fn [result]
|
||||
(reset! warning-info* (assoc result :organization-id org-id)))
|
||||
(reset! warning-info* (assoc result :organization-id organization-id)))
|
||||
(fn [_]
|
||||
(reset! warning-info* nil))))))))
|
||||
|
||||
@ -947,22 +947,22 @@
|
||||
(fn []
|
||||
(on-confirm (dm/get-in @form [:clean-data :selected-id]))))]
|
||||
[:div {:class (stl/css :modal-overlay)}
|
||||
[:div {:class (stl/css :modal-select-org-container :modal-container)}
|
||||
[:div {:class (stl/css :modal-select-org-header)}
|
||||
[:h2 {:class (stl/css :modal-select-org-title)}
|
||||
[:div {:class (stl/css :modal-select-organization-container :modal-container)}
|
||||
[:div {:class (stl/css :modal-select-organization-header)}
|
||||
[:h2 {:class (stl/css :modal-select-organization-title)}
|
||||
(tr title-key)]
|
||||
|
||||
[:button {:class (stl/css :modal-close-btn)
|
||||
:on-click modal/hide!} deprecated-icon/close]]
|
||||
|
||||
(when text-key
|
||||
[:div {:class (stl/css :modal-content :modal-select-org-text)} (tr text-key)])
|
||||
[:div {:class (stl/css :modal-content :modal-select-organization-text)} (tr text-key)])
|
||||
|
||||
[:div {:class (stl/css :modal-select-org-body)}
|
||||
[:div {:class (stl/css :modal-select-organization-body)}
|
||||
(when info-message-key
|
||||
[:div {:class (stl/css :modal-select-org-info)}
|
||||
[:div {:class (stl/css :modal-select-organization-info)}
|
||||
(tr info-message-key)])
|
||||
[:div {:class (stl/css :modal-select-org-content)}
|
||||
[:div {:class (stl/css :modal-select-organization-content)}
|
||||
(tr choose-key)]
|
||||
[:> combobox* {:id "selected-id"
|
||||
:class (stl/css :team-member)
|
||||
@ -976,14 +976,14 @@
|
||||
(when (and warning-info
|
||||
(:has-external-invitations warning-info)
|
||||
(not (:allows-anybody warning-info))
|
||||
selected-org)
|
||||
[:div {:class (stl/css :modal-select-org-warning)}
|
||||
selected-organization)
|
||||
[:div {:class (stl/css :modal-select-organization-warning)}
|
||||
[:& context-notification
|
||||
{:content (tr "dashboard.select-org-modal.external-invitations-will-be-canceled")
|
||||
{:content (tr "dashboard.select-organization-modal.external-invitations-will-be-canceled")
|
||||
:class (stl/css :external-invitations-warning)
|
||||
:level :warning}]
|
||||
[:div {:class (stl/css :modal-select-org-content)}
|
||||
(tr "dashboard.select-org-modal.external-invitations-warning" (:name selected-org))]])]
|
||||
[:div {:class (stl/css :modal-select-organization-content)}
|
||||
(tr "dashboard.select-organization-modal.external-invitations-warning" (:name selected-organization))]])]
|
||||
|
||||
[:div {:class (stl/css :modal-footer)}
|
||||
[:div {:class (stl/css :action-buttons :modal-invitation-action-buttons)}
|
||||
@ -1526,15 +1526,15 @@
|
||||
(filter :organization)
|
||||
(map dtm/team->organization)))
|
||||
|
||||
;; Filter to orgs where user is allowed to create/add teams
|
||||
;; Filter to organizations where user is allowed to create/add teams
|
||||
organizations (mf/with-memo [all-organizations profile-id]
|
||||
(->> all-organizations
|
||||
(filter (fn [org]
|
||||
(let [perm (get-in org [:permissions :create-teams])
|
||||
is-owner? (= profile-id (:owner-id org))]
|
||||
(filter (fn [organization]
|
||||
(let [perm (get-in organization [:permissions :create-teams])
|
||||
is-owner? (= profile-id (:owner-id organization))]
|
||||
(or (= perm "any") is-owner?))))))
|
||||
|
||||
;; Keep parity with UX requirement: hide only when user belongs to one org.
|
||||
;; Keep parity with UX requirement: hide only when user belongs to one organization.
|
||||
can-change-organization? (mf/with-memo [all-organizations]
|
||||
(> (count all-organizations) 1))
|
||||
|
||||
@ -1543,20 +1543,20 @@
|
||||
(pos? (count all-organizations))
|
||||
(not (:is-default team))))
|
||||
|
||||
show-org-options-menu*
|
||||
show-organization-options-menu*
|
||||
(mf/use-state false)
|
||||
|
||||
show-org-options-menu?
|
||||
(deref show-org-options-menu*)
|
||||
show-organization-options-menu?
|
||||
(deref show-organization-options-menu*)
|
||||
|
||||
on-show-options-click
|
||||
(mf/use-fn
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(swap! show-org-options-menu* not)))
|
||||
(swap! show-organization-options-menu* not)))
|
||||
|
||||
close-org-options-menu
|
||||
(mf/use-fn #(reset! show-org-options-menu* false))
|
||||
close-organization-options-menu
|
||||
(mf/use-fn #(reset! show-organization-options-menu* false))
|
||||
|
||||
on-image-click
|
||||
(mf/use-fn #(dom/click (mf/ref-val finput)))
|
||||
@ -1565,11 +1565,11 @@
|
||||
(fn [file]
|
||||
(st/emit! (dtm/update-team-photo file)))
|
||||
|
||||
on-remove-team-from-org
|
||||
on-remove-team-from-organization
|
||||
(mf/use-fn
|
||||
(mf/deps team)
|
||||
(fn []
|
||||
(st/emit! (dnt/show-remove-team-from-org-modal {:team-id (:id team)}))))
|
||||
(st/emit! (dnt/show-remove-team-from-organization-modal {:team-id (:id team)}))))
|
||||
|
||||
on-add-team-to-organization
|
||||
(mf/use-fn
|
||||
@ -1577,11 +1577,11 @@
|
||||
(fn []
|
||||
(st/emit! (dnt/show-add-team-to-organization-modal {:team-id (:id team)}))))
|
||||
|
||||
on-change-team-org
|
||||
on-change-team-organization
|
||||
(mf/use-fn
|
||||
(mf/deps team)
|
||||
(fn []
|
||||
(st/emit! (dnt/show-change-team-org-modal {:team-id (:id team)}))))]
|
||||
(st/emit! (dnt/show-change-team-organization-modal {:team-id (:id team)}))))]
|
||||
|
||||
(mf/with-effect [team]
|
||||
(dom/set-html-title (tr "title.team-settings"
|
||||
@ -1622,8 +1622,8 @@
|
||||
(let [organization (:organization team)]
|
||||
(if organization
|
||||
[:div {:class (stl/css :block-content)}
|
||||
[:div {:class (stl/css :org-block-content)}
|
||||
[:> org-avatar* {:org (dtm/team->organization team) :size "xxxl"}]
|
||||
[:div {:class (stl/css :organization-block-content)}
|
||||
[:> organization-avatar* {:organization (dtm/team->organization team) :size "xxxl"}]
|
||||
[:span {:class (stl/css :block-text)}
|
||||
(:name organization)]
|
||||
|
||||
@ -1631,19 +1631,19 @@
|
||||
[:*
|
||||
[:> button* {:variant "ghost"
|
||||
:type "button"
|
||||
:class (stl/css-case :org-options-btn (not show-org-options-menu?) :org-options-btn-open show-org-options-menu?)
|
||||
:class (stl/css-case :organization-options-btn (not show-organization-options-menu?) :organization-options-btn-open show-organization-options-menu?)
|
||||
:on-click on-show-options-click}
|
||||
org-menu-icon
|
||||
organization-menu-icon
|
||||
|
||||
[:& dropdown {:show show-org-options-menu? :on-close close-org-options-menu :dropdown-id "org-options"}
|
||||
[:ul {:class (stl/css :org-dropdown)
|
||||
[:& dropdown {:show show-organization-options-menu? :on-close close-organization-options-menu :dropdown-id "organization-options"}
|
||||
[:ul {:class (stl/css :organization-dropdown)
|
||||
:role "listbox"}
|
||||
(when can-change-organization?
|
||||
[:li {:on-click on-change-team-org
|
||||
:class (stl/css :org-dropdown-item)}
|
||||
[:li {:on-click on-change-team-organization
|
||||
:class (stl/css :organization-dropdown-item)}
|
||||
(tr "dashboard.team-organization.change")])
|
||||
[:li {:on-click on-remove-team-from-org
|
||||
:class (stl/css :org-dropdown-item)}
|
||||
[:li {:on-click on-remove-team-from-organization
|
||||
:class (stl/css :organization-dropdown-item)}
|
||||
(tr "dashboard.team-organization.remove")]]]]])]]
|
||||
[:*
|
||||
[:div {:class (stl/css :block-content)}
|
||||
|
||||
@ -932,47 +932,47 @@
|
||||
|
||||
// SELECT ORGANIZATION MODAL
|
||||
|
||||
.modal-select-org-container {
|
||||
.modal-select-organization-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
inline-size: $sz-512;
|
||||
}
|
||||
|
||||
.modal-select-org-header {
|
||||
.modal-select-organization-header {
|
||||
margin-block-end: var(--sp-xxxl);
|
||||
}
|
||||
|
||||
.modal-select-org-body {
|
||||
.modal-select-organization-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-s);
|
||||
}
|
||||
|
||||
.modal-select-org-warning {
|
||||
.modal-select-organization-warning {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-xxl);
|
||||
}
|
||||
|
||||
.modal-select-org-content {
|
||||
.modal-select-organization-content {
|
||||
@include t.use-typography("body-medium");
|
||||
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
.modal-select-org-info {
|
||||
.modal-select-organization-info {
|
||||
@include t.use-typography("body-medium");
|
||||
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
.modal-select-org-title {
|
||||
.modal-select-organization-title {
|
||||
@include t.use-typography("headline-large");
|
||||
|
||||
color: var(--color-foreground-primary);
|
||||
}
|
||||
|
||||
.modal-select-org-text {
|
||||
.modal-select-organization-text {
|
||||
@include t.use-typography("body-medium");
|
||||
|
||||
color: var(--color-foreground-secondary);
|
||||
@ -984,7 +984,7 @@
|
||||
|
||||
// ORGANIZATIONS SETTINGS
|
||||
|
||||
.org-block-content {
|
||||
.organization-block-content {
|
||||
display: grid;
|
||||
grid-template-columns: var(--sp-xxxl) 1fr var(--sp-xxxl);
|
||||
align-items: center;
|
||||
@ -992,7 +992,7 @@
|
||||
inline-size: max-content;
|
||||
}
|
||||
|
||||
.org-options-btn {
|
||||
.organization-options-btn {
|
||||
padding: 0;
|
||||
justify-content: center;
|
||||
|
||||
@ -1003,7 +1003,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.org-options-btn-open {
|
||||
.organization-options-btn-open {
|
||||
padding: 0;
|
||||
justify-content: center;
|
||||
|
||||
@ -1013,7 +1013,7 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.org-menu-icon {
|
||||
.organization-menu-icon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@ -1025,7 +1025,7 @@
|
||||
stroke: var(--stroke-color);
|
||||
}
|
||||
|
||||
.org-dropdown {
|
||||
.organization-dropdown {
|
||||
box-shadow: var(--el-shadow-dark);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -1043,7 +1043,7 @@
|
||||
min-inline-size: $sz-160;
|
||||
}
|
||||
|
||||
.org-dropdown-item {
|
||||
.organization-dropdown-item {
|
||||
@include t.use-typography("body-small");
|
||||
|
||||
display: flex;
|
||||
|
||||
@ -152,10 +152,10 @@
|
||||
(tr "dashboard.no-permission-create-team.message" organization-name)]
|
||||
:delete-team [(tr "dashboard.delete-team")
|
||||
(tr "dashboard.no-permission-delete-team.message" organization-name)]
|
||||
:no-orgs-create [(tr "dashboard.select-org-modal.title")
|
||||
(tr "dashboard.no-org-allows-create-team.message")]
|
||||
:no-orgs-change [(tr "dashboard.change-org-modal.title")
|
||||
(tr "dashboard.no-permission-move-team.message" organization-name)])]
|
||||
:no-organizations-create [(tr "dashboard.select-organization-modal.title")
|
||||
(tr "dashboard.no-organization-allows-create-team.message")]
|
||||
:no-organizations-change [(tr "dashboard.change-organization-modal.title")
|
||||
(tr "dashboard.no-permission-move-team.message" organization-name)])]
|
||||
[:div {:class (stl/css :modal-overlay)}
|
||||
[:div {:class (stl/css :modal-container)}
|
||||
[:div {:class (stl/css :modal-header)}
|
||||
|
||||
@ -61,23 +61,23 @@ Avatar rendering is defined per option with `:render-fn`, so each avatar type ca
|
||||
|
||||
```clj
|
||||
;; Example renderer for organization avatars
|
||||
(mf/defc render-org-avatar*
|
||||
(mf/defc render-organization-avatar*
|
||||
[{:keys [avatar]}]
|
||||
(when (= :organization (:type avatar))
|
||||
[:> org-avatar* {:org (:organization avatar)
|
||||
:size (:size avatar)}]))
|
||||
[:> organization-avatar* {:organization (:organization avatar)
|
||||
:size (:size avatar)}]))
|
||||
|
||||
[:> combobox*
|
||||
{:options [{:label "Design Team"
|
||||
:id "org-design"
|
||||
:avatar {:render-fn render-org-avatar*
|
||||
:id "organization-design"
|
||||
:avatar {:render-fn render-organization-avatar*
|
||||
:size "s"
|
||||
:organization {:name "Design Team"
|
||||
:organization-avatar-bg-url "https://example.com/avatar-bg.svg"
|
||||
:organization-custom-photo nil}}}
|
||||
{:label "Engineering"
|
||||
:id "org-engineering"
|
||||
:avatar {:render-fn render-org-avatar*
|
||||
:id "organization-engineering"
|
||||
:avatar {:render-fn render-organization-avatar*
|
||||
:size "s"
|
||||
:organization {:name "Engineering"
|
||||
:organization-avatar-bg-url nil
|
||||
|
||||
@ -32,7 +32,7 @@
|
||||
date-str (when cancel-at
|
||||
(ct/format-inst cancel-at "d MMMM, yyyy"))
|
||||
|
||||
on-create-org
|
||||
on-create-organization
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
(modal/hide!)
|
||||
@ -64,6 +64,6 @@
|
||||
(tr "nitrate.activation-success.enjoy")]
|
||||
|
||||
[:> button* {:variant "primary"
|
||||
:on-click on-create-org
|
||||
:on-click on-create-organization
|
||||
:class (stl/css :modal-button)}
|
||||
(tr "nitrate.activation-success.create-org")]]]]]))
|
||||
(tr "nitrate.activation-success.create-organization")]]]]]))
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
[app.main.data.profile :as du]
|
||||
[app.main.repo :as rp]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.org-avatar :refer [org-avatar*]]
|
||||
[app.main.ui.components.organization-avatar :refer [organization-avatar*]]
|
||||
[app.main.ui.ds.foundations.assets.icon :as i :refer [icon*]]
|
||||
[app.main.ui.icons :as deprecated-icon]
|
||||
[app.main.ui.notifications.context-notification :refer [context-notification]]
|
||||
@ -33,9 +33,9 @@
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :delete-account}
|
||||
[]
|
||||
(let [orgs* (mf/use-state nil)
|
||||
orgs (deref orgs*)
|
||||
has-orgs? (seq orgs)
|
||||
(let [organizations* (mf/use-state nil)
|
||||
organizations (deref organizations*)
|
||||
has-organizations? (seq organizations)
|
||||
|
||||
expanded* (mf/use-state true)
|
||||
expanded? (deref expanded*)
|
||||
@ -51,11 +51,11 @@
|
||||
(if (contains? cf/flags :nitrate)
|
||||
(let [sub (->> (rp/cmd! :get-owned-organizations-summary {})
|
||||
(rx/subs!
|
||||
(fn [result] (reset! orgs* (or result [])))
|
||||
(fn [_] (reset! orgs* []))))]
|
||||
(fn [result] (reset! organizations* (or result [])))
|
||||
(fn [_] (reset! organizations* []))))]
|
||||
(fn []
|
||||
(rx/dispose! sub)))
|
||||
(reset! orgs* [])))
|
||||
(reset! organizations* [])))
|
||||
|
||||
[:div {:class (stl/css :modal-overlay)}
|
||||
[:div {:class (stl/css :modal-container)}
|
||||
@ -69,33 +69,33 @@
|
||||
[:div {:class (stl/css :warning-notice)}
|
||||
[:& context-notification
|
||||
{:level :warning
|
||||
:content (tr (if has-orgs?
|
||||
"modals.delete-account.info.with-orgs"
|
||||
:content (tr (if has-organizations?
|
||||
"modals.delete-account.info.with-organizations"
|
||||
"modals.delete-account.info"))}]]
|
||||
|
||||
(when has-orgs?
|
||||
[:div {:class (stl/css :orgs-section)}
|
||||
[:button {:class (stl/css :orgs-section-toggle)
|
||||
(when has-organizations?
|
||||
[:div {:class (stl/css :organizations-section)}
|
||||
[:button {:class (stl/css :organizations-section-toggle)
|
||||
:type "button"
|
||||
:aria-expanded expanded?
|
||||
:on-click on-toggle}
|
||||
[:span {:class (stl/css :orgs-section-title)}
|
||||
(tr "modals.delete-account.owned-orgs.list-title")]
|
||||
[:span {:class (stl/css :organizations-section-title)}
|
||||
(tr "modals.delete-account.owned-organizations.list-title")]
|
||||
[:> icon* {:icon-id i/arrow
|
||||
:size "s"
|
||||
:class (stl/css-case :orgs-section-arrow true
|
||||
:class (stl/css-case :organizations-section-arrow true
|
||||
:expanded expanded?)}]]
|
||||
(when expanded?
|
||||
[:ul {:class (stl/css :org-list)}
|
||||
(for [{:keys [id name team-count member-count] :as org} orgs]
|
||||
[:li {:class (stl/css :org-item) :key id}
|
||||
[:> org-avatar* {:org org :size "xxl"}]
|
||||
[:div {:class (stl/css :org-info)}
|
||||
[:span {:class (stl/css :org-name)} name]
|
||||
[:div {:class (stl/css :org-counts)}
|
||||
[:span (tr "modals.delete-account.owned-orgs.teams-count"
|
||||
[:ul {:class (stl/css :organization-list)}
|
||||
(for [{:keys [id name team-count member-count] :as organization} organizations]
|
||||
[:li {:class (stl/css :organization-item) :key id}
|
||||
[:> organization-avatar* {:organization organization :size "xxl"}]
|
||||
[:div {:class (stl/css :organization-info)}
|
||||
[:span {:class (stl/css :organization-name)} name]
|
||||
[:div {:class (stl/css :organization-counts)}
|
||||
[:span (tr "modals.delete-account.owned-organizations.teams-count"
|
||||
(i18n/c (or team-count 0)))]
|
||||
[:span (tr "modals.delete-account.owned-orgs.members-count"
|
||||
[:span (tr "modals.delete-account.owned-organizations.members-count"
|
||||
(i18n/c (or member-count 0)))]]]])])])]
|
||||
|
||||
[:div {:class (stl/css :modal-footer)}
|
||||
|
||||
@ -60,7 +60,7 @@
|
||||
color: var(--modal-title-foreground-color);
|
||||
}
|
||||
|
||||
.orgs-section {
|
||||
.organizations-section {
|
||||
@include t.use-typography("body-medium");
|
||||
|
||||
display: flex;
|
||||
@ -70,7 +70,7 @@
|
||||
gap: var(--sp-l);
|
||||
}
|
||||
|
||||
.orgs-section-toggle {
|
||||
.organizations-section-toggle {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
@ -81,20 +81,20 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.orgs-section-title {
|
||||
.organizations-section-title {
|
||||
color: var(--color-accent-tertiary);
|
||||
}
|
||||
|
||||
.orgs-section-arrow {
|
||||
.organizations-section-arrow {
|
||||
color: var(--color-accent-tertiary);
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
|
||||
.orgs-section-arrow.expanded {
|
||||
.organizations-section-arrow.expanded {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.org-list {
|
||||
.organization-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 auto;
|
||||
@ -106,23 +106,23 @@
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.org-item {
|
||||
.organization-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: var(--sp-s);
|
||||
}
|
||||
|
||||
.org-info {
|
||||
.organization-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.org-name {
|
||||
.organization-name {
|
||||
color: var(--modal-title-foreground-color);
|
||||
}
|
||||
|
||||
.org-counts {
|
||||
.organization-counts {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--sp-s);
|
||||
|
||||
@ -612,7 +612,7 @@
|
||||
:cancel-at (when (:cancel-at nitrate-license)
|
||||
(tr "nitrate.subscription.active-until" (ct/format-inst (:cancel-at nitrate-license) "d MMMM, yyyy")))
|
||||
:benefits-title (tr "subscription.settings.benefits.nitrate-unlimited-benefits")
|
||||
:benefits [{:label (tr "subscription.settings.enterprise.nitrate.multi-org-management")
|
||||
:benefits [{:label (tr "subscription.settings.enterprise.nitrate.multi-organization-management")
|
||||
:description (tr "subscription.settings.enterprise.nitrate.support-team")}
|
||||
{:label (tr "subscription.settings.enterprise.nitrate.enterprise-security")
|
||||
:description (tr "subscription.settings.enterprise.nitrate.native-sso")}
|
||||
@ -770,7 +770,7 @@
|
||||
:price-value "$25"
|
||||
:price-period (tr "subscription.settings.organization-member-month")
|
||||
:benefits-title (tr "subscription.settings.benefits.nitrate-unlimited-benefits")
|
||||
:benefits [{:label (tr "subscription.settings.enterprise.nitrate.multi-org-management")
|
||||
:benefits [{:label (tr "subscription.settings.enterprise.nitrate.multi-organization-management")
|
||||
:description (tr "subscription.settings.enterprise.nitrate.support-team")}
|
||||
{:label (tr "subscription.settings.enterprise.nitrate.enterprise-security")
|
||||
:description (tr "subscription.settings.enterprise.nitrate.native-sso")}
|
||||
@ -877,7 +877,7 @@
|
||||
[:div {:class (stl/css :modal-text-medium)}
|
||||
(tr "nitrate.contact-sales.downgrade-title")]
|
||||
[:ul {:class (stl/css :downgrade-list)}
|
||||
[:li {:class (stl/css :downgrade-item)} (tr "nitrate.contact-sales.downgrade-org-deleted")]
|
||||
[:li {:class (stl/css :downgrade-item)} (tr "nitrate.contact-sales.downgrade-organization-deleted")]
|
||||
[:li {:class (stl/css :downgrade-item)} (tr "nitrate.contact-sales.downgrade-teams-available")]
|
||||
[:li {:class (stl/css :downgrade-item)} (tr "nitrate.contact-sales.downgrade-storage-limited")]]
|
||||
|
||||
|
||||
@ -23,7 +23,7 @@
|
||||
current-url (str "https://penpot.example.com/#/dashboard/recent?team-id=" team-id)
|
||||
redirect-url "https://idp.example.com/authorize"
|
||||
state {:current-team-id team-id}
|
||||
event (dcm/handle-change-team-org
|
||||
event (dcm/handle-change-team-organization
|
||||
{:team {:id team-id :organization organization}
|
||||
:notification nil})]
|
||||
(mock/with-mocks
|
||||
@ -52,14 +52,14 @@
|
||||
(t/deftest organization-sso-activation-redirects-current-team
|
||||
(t/async done
|
||||
(let [team-id (uuid/next)
|
||||
org-id (uuid/next)
|
||||
organization-id (uuid/next)
|
||||
current-url (str "https://penpot.example.com/#/workspace?team-id=" team-id)
|
||||
redirect-url "https://idp.example.com/authorize"
|
||||
state {:current-team-id team-id
|
||||
:teams {team-id {:id team-id
|
||||
:organization {:id org-id}}}}
|
||||
:organization {:id organization-id}}}}
|
||||
event (dcm/handle-organization-change-sso
|
||||
{:organization-id org-id})]
|
||||
{:organization-id organization-id})]
|
||||
(mock/with-mocks
|
||||
{cf/flags (conj cf/flags :nitrate)
|
||||
rp/cmd! (mock/stub
|
||||
|
||||
@ -147,10 +147,10 @@
|
||||
(let [public-uri (u/uri "https://example.com/penpot/")]
|
||||
(t/is (= "https://example.com/penpot/admin-console/"
|
||||
(dnt/build-admin-console-url public-uri "" nil)))
|
||||
(t/is (= "https://example.com/penpot/admin-console/organization/my-org/organization-id/people/"
|
||||
(t/is (= "https://example.com/penpot/admin-console/organization/my-organization/organization-id/people/"
|
||||
(dnt/build-admin-console-url
|
||||
public-uri
|
||||
"organization/my-org/organization-id/people/"
|
||||
"organization/my-organization/organization-id/people/"
|
||||
nil)))
|
||||
(t/is (= {:action "create-organization"
|
||||
:origin "dashboard:organization-switcher"}
|
||||
|
||||
@ -370,7 +370,7 @@ msgid "dashboard.copy-suffix"
|
||||
msgstr "(Kopie)"
|
||||
|
||||
#: src/app/main/ui/dashboard/sidebar.cljs:371
|
||||
msgid "dashboard.create-new-org"
|
||||
msgid "dashboard.create-new-organization"
|
||||
msgstr "Neue Organisation erstellen"
|
||||
|
||||
#: src/app/main/ui/dashboard/sidebar.cljs:439
|
||||
|
||||
@ -98,7 +98,7 @@ msgid "auth.new-password"
|
||||
msgstr "Type a new password"
|
||||
|
||||
#: src/app/main/ui/auth/verify_token.cljs:53
|
||||
msgid "auth.notifications.org-invitation-accepted"
|
||||
msgid "auth.notifications.organization-invitation-accepted"
|
||||
msgstr "You're now part of %s"
|
||||
|
||||
#: src/app/main/ui/auth/recovery.cljs:36
|
||||
@ -297,7 +297,7 @@ msgid "common.unpublish"
|
||||
msgstr "Unpublish"
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:165
|
||||
msgid "dasboard.leave-org.toast"
|
||||
msgid "dashboard.leave-organization.toast"
|
||||
msgstr "You're no longer part of the %s organization."
|
||||
|
||||
#: src/app/main/ui/dashboard/projects.cljs:90
|
||||
@ -367,25 +367,25 @@ msgid "dashboard.change-email"
|
||||
msgstr "Change email"
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.change-org-modal.accept"
|
||||
msgid "dashboard.change-organization-modal.accept"
|
||||
msgstr "MOVE TEAM"
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.change-org-modal.choose"
|
||||
msgid "dashboard.change-organization-modal.choose"
|
||||
msgstr "Move to:"
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.change-org-modal.select"
|
||||
msgid "dashboard.change-organization-modal.select"
|
||||
msgstr "Select an organization"
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.change-org-modal.text"
|
||||
msgid "dashboard.change-organization-modal.text"
|
||||
msgstr ""
|
||||
"Projects and files will remain available to team members. The team will get "
|
||||
"the configuration from the new organization."
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:399, src/app/main/ui/dashboard/team_form.cljs:157
|
||||
msgid "dashboard.change-org-modal.title"
|
||||
msgid "dashboard.change-organization-modal.title"
|
||||
msgstr "Change team's organization"
|
||||
|
||||
#: src/app/main/ui/dashboard/deleted.cljs:316
|
||||
@ -397,7 +397,7 @@ msgid "dashboard.copy-suffix"
|
||||
msgstr "(copy)"
|
||||
|
||||
#: src/app/main/ui/dashboard/sidebar.cljs:371
|
||||
msgid "dashboard.create-new-org"
|
||||
msgid "dashboard.create-new-organization"
|
||||
msgstr "Create org"
|
||||
|
||||
#: src/app/main/ui/dashboard/sidebar.cljs:439
|
||||
@ -816,7 +816,7 @@ msgid "dashboard.invite-profile"
|
||||
msgstr "Invite people"
|
||||
|
||||
#: src/app/main/ui/dashboard/sidebar.cljs:671
|
||||
msgid "dashboard.leave-org"
|
||||
msgid "dashboard.leave-organization"
|
||||
msgstr "Leave org"
|
||||
|
||||
#: src/app/main/ui/dashboard/sidebar.cljs:570, src/app/main/ui/dashboard/sidebar.cljs:577, src/app/main/ui/dashboard/sidebar.cljs:582, src/app/main/ui/dashboard/team.cljs:433
|
||||
@ -892,7 +892,7 @@ msgid "dashboard.no-matches-for"
|
||||
msgstr "No matches found for “%s“"
|
||||
|
||||
#: src/app/main/ui/dashboard/team_form.cljs:156
|
||||
msgid "dashboard.no-org-allows-create-team.message"
|
||||
msgid "dashboard.no-organization-allows-create-team.message"
|
||||
msgstr "You don't have permission to add teams to any of your organizations."
|
||||
|
||||
#: src/app/main/ui/dashboard/team_form.cljs:152
|
||||
@ -962,7 +962,7 @@ msgid "dashboard.order-invitations-by-status"
|
||||
msgstr "Order by status"
|
||||
|
||||
#: src/app/main/data/dashboard.cljs:753
|
||||
msgid "dashboard.org-deleted"
|
||||
msgid "dashboard.organization-deleted"
|
||||
msgstr "The %s organization has been deleted."
|
||||
|
||||
#: src/app/main/ui/settings/password.cljs:96, src/app/main/ui/settings/password.cljs:109
|
||||
@ -1002,7 +1002,7 @@ msgid "dashboard.plugins.try-plugin"
|
||||
msgstr "Try plugin: "
|
||||
|
||||
#: src/app/main/ui/dashboard/sidebar.cljs:798
|
||||
msgid "dashboard.plus-create-new-org"
|
||||
msgid "dashboard.plus-create-new-organization"
|
||||
msgstr "+ Create org"
|
||||
|
||||
#: src/app/main/data/dashboard.cljs:805
|
||||
@ -1090,35 +1090,35 @@ msgid "dashboard.section.teams"
|
||||
msgstr "Teams"
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.select-org-modal.accept"
|
||||
msgid "dashboard.select-organization-modal.accept"
|
||||
msgstr "ADD TO ORGANIZATION"
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.select-org-modal.choose"
|
||||
msgid "dashboard.select-organization-modal.choose"
|
||||
msgstr "Choose an organization:"
|
||||
|
||||
#: src/app/main/ui/dashboard/team.cljs:975
|
||||
msgid "dashboard.select-org-modal.external-invitations-warning"
|
||||
msgid "dashboard.select-organization-modal.external-invitations-warning"
|
||||
msgstr ""
|
||||
"\"%s\" only allows organization members to join its teams. Any pending "
|
||||
"invitations to people outside the organization will be revoked."
|
||||
|
||||
#: src/app/main/ui/dashboard/team.cljs:971
|
||||
msgid "dashboard.select-org-modal.external-invitations-will-be-canceled"
|
||||
msgid "dashboard.select-organization-modal.external-invitations-will-be-canceled"
|
||||
msgstr "Pending invitations to external users will be canceled."
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.select-org-modal.permission-info"
|
||||
msgid "dashboard.select-organization-modal.permission-info"
|
||||
msgstr ""
|
||||
"Here you find all your organizations where you are allowed to create or add "
|
||||
"teams."
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.select-org-modal.select"
|
||||
msgid "dashboard.select-organization-modal.select"
|
||||
msgstr "Select an organization"
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:342, src/app/main/ui/dashboard/team_form.cljs:155
|
||||
msgid "dashboard.select-org-modal.title"
|
||||
msgid "dashboard.select-organization-modal.title"
|
||||
msgstr "Add team to an organization"
|
||||
|
||||
#: src/app/main/ui/settings/options.cljs:66
|
||||
@ -1242,7 +1242,7 @@ msgid "dashboard.success-move-project"
|
||||
msgstr "Your project has been moved successfully"
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.team-belong-org"
|
||||
msgid "dashboard.team-belong-organization"
|
||||
msgstr "This team now belongs to %s"
|
||||
|
||||
#: src/app/main/ui/dashboard/team.cljs:1602
|
||||
@ -1254,7 +1254,7 @@ msgid "dashboard.team-members"
|
||||
msgstr "Team members"
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.team-no-longer-belong-org"
|
||||
msgid "dashboard.team-no-longer-belong-organization"
|
||||
msgstr "This team no longer belongs to the organization %s"
|
||||
|
||||
#: src/app/main/ui/dashboard/team.cljs:1609
|
||||
@ -1266,7 +1266,7 @@ msgid "dashboard.team-organization.add"
|
||||
msgstr "Add to an organization"
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:341, src/app/main/data/nitrate.cljs:398
|
||||
msgid "dashboard.team-organization.add.no-valid-orgs"
|
||||
msgid "dashboard.team-organization.add.no-valid-organizations"
|
||||
msgstr "You don't have permission to add this team to any of your organizations."
|
||||
|
||||
#: src/app/main/ui/dashboard/team.cljs:1632
|
||||
@ -1274,7 +1274,7 @@ msgid "dashboard.team-organization.change"
|
||||
msgstr "Change team organization"
|
||||
|
||||
#: src/app/main/ui/dashboard/team.cljs:905
|
||||
msgid "dashboard.team-organization.disabled-org-tooltip"
|
||||
msgid "dashboard.team-organization.disabled-organization-tooltip"
|
||||
msgstr "Only people within your organization can be invited."
|
||||
|
||||
#: src/app/main/ui/dashboard/team.cljs:1639
|
||||
@ -1340,7 +1340,7 @@ msgid "dashboard.upgrade-plan.penpot-free"
|
||||
msgstr "Penpot Free"
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.user-no-longer-belong-org"
|
||||
msgid "dashboard.user-no-longer-belong-organization"
|
||||
msgstr "You are no longer a member of the organization %s"
|
||||
|
||||
#: src/app/main/ui/settings/options.cljs:109
|
||||
@ -1727,15 +1727,15 @@ msgid "errors.only-creator-can-unlock"
|
||||
msgstr "Only the version creator can unlock it"
|
||||
|
||||
#, unused
|
||||
msgid "errors.org-leave.no-valid-teams"
|
||||
msgid "errors.organization-leave.no-valid-teams"
|
||||
msgstr "There was a problem leaving the organization. Please try again."
|
||||
|
||||
#, unused
|
||||
msgid "errors.org-leave.org-owner-cannot-leave"
|
||||
msgid "errors.organization-leave.organization-owner-cannot-leave"
|
||||
msgstr "The organization owner can't leave the organization."
|
||||
|
||||
#: src/app/main/ui/auth/verify_token.cljs:97
|
||||
msgid "errors.org-not-found"
|
||||
msgid "errors.organization-not-found"
|
||||
msgstr "That organization doesn't exists"
|
||||
|
||||
#: src/app/main/ui/settings/password.cljs
|
||||
@ -3763,17 +3763,17 @@ msgid "modals.add-shared-confirm.message"
|
||||
msgstr "Add “%s” as Shared Library"
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:199
|
||||
msgid "modals.before-leave-org.message"
|
||||
msgid "modals.before-leave-organization.message"
|
||||
msgstr ""
|
||||
"You are the only member of some of the teams. Those teams will be deleted "
|
||||
"along with its projects and files."
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:198, src/app/main/ui/dashboard/change_owner.cljs:151
|
||||
msgid "modals.before-leave-org.title"
|
||||
msgid "modals.before-leave-organization.title"
|
||||
msgstr "BEFORE YOU LEAVE THE ORGANIZATION"
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:202
|
||||
msgid "modals.before-leave-org.warning"
|
||||
msgid "modals.before-leave-organization.warning"
|
||||
msgstr "Any team where you’re the only member will be deleted."
|
||||
|
||||
#: src/app/main/ui/workspace/nudge.cljs:59
|
||||
@ -3831,22 +3831,22 @@ msgstr "By removing your account you’ll lose all your current projects and arc
|
||||
|
||||
#: src/app/main/ui/settings/delete_account.cljs
|
||||
#, unused
|
||||
msgid "modals.delete-account.info.with-orgs"
|
||||
msgid "modals.delete-account.info.with-organizations"
|
||||
msgstr ""
|
||||
"This action is irreversible. Your account, subscription, and organizations "
|
||||
"will be deleted immediately. You will also lose access to your current "
|
||||
"projects and files."
|
||||
|
||||
#: src/app/main/ui/settings/delete_account.cljs:83
|
||||
msgid "modals.delete-account.owned-orgs.list-title"
|
||||
msgid "modals.delete-account.owned-organizations.list-title"
|
||||
msgstr "Organizations that will be deleted"
|
||||
|
||||
#: src/app/main/ui/settings/delete_account.cljs:98
|
||||
msgid "modals.delete-account.owned-orgs.members-count"
|
||||
msgid "modals.delete-account.owned-organizations.members-count"
|
||||
msgstr "%s members"
|
||||
|
||||
#: src/app/main/ui/settings/delete_account.cljs:96
|
||||
msgid "modals.delete-account.owned-orgs.teams-count"
|
||||
msgid "modals.delete-account.owned-organizations.teams-count"
|
||||
msgstr "%s teams"
|
||||
|
||||
#: src/app/main/ui/settings/delete_account.cljs:64
|
||||
@ -3930,7 +3930,7 @@ msgid "modals.delete-font.title"
|
||||
msgstr "Deleting font"
|
||||
|
||||
#: src/app/main/data/team.cljs:129
|
||||
msgid "modals.delete-org-team-confirm.message"
|
||||
msgid "modals.delete-organization-team-confirm.message"
|
||||
msgstr "Are you sure you want to delete this team that is part of %s org?"
|
||||
|
||||
#: src/app/main/ui/workspace/context_menu.cljs:707, src/app/main/ui/workspace/sidebar/sitemap.cljs:109
|
||||
@ -4082,7 +4082,7 @@ msgid "modals.invite-restricted-members.all-blocked-title"
|
||||
msgstr "No invitations were sent"
|
||||
|
||||
#: src/app/main/data/team.cljs:162
|
||||
msgid "modals.invite-restricted-members.all-org-members-in-team"
|
||||
msgid "modals.invite-restricted-members.all-organization-members-in-team"
|
||||
msgstr ""
|
||||
"Only \"%s\" organization's members can be invited to this team, and they "
|
||||
"are all already team members. If you need more information, contact the "
|
||||
@ -4169,35 +4169,35 @@ msgid "modals.leave-confirm.title"
|
||||
msgstr "Leaving team"
|
||||
|
||||
#: src/app/main/ui/dashboard/change_owner.cljs:158
|
||||
msgid "modals.leave-org-and-reassign.hint"
|
||||
msgid "modals.leave-organization-and-reassign.hint"
|
||||
msgstr ""
|
||||
"You are the owner of some organization's teams. Please promote another "
|
||||
"member to become an owner."
|
||||
|
||||
#: src/app/main/ui/dashboard/change_owner.cljs:161
|
||||
msgid "modals.leave-org-and-reassign.hint-delete"
|
||||
msgid "modals.leave-organization-and-reassign.hint-delete"
|
||||
msgstr ""
|
||||
"You are the only member of some of the teams. Those teams will be deleted "
|
||||
"along with its projects and files."
|
||||
|
||||
#: src/app/main/ui/dashboard/change_owner.cljs:163
|
||||
msgid "modals.leave-org-and-reassign.hint-promote"
|
||||
msgid "modals.leave-organization-and-reassign.hint-promote"
|
||||
msgstr ""
|
||||
"Also, you are the owner of some organization's teams. Please promote "
|
||||
"another member to become an owner."
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:200, src/app/main/data/nitrate.cljs:209
|
||||
msgid "modals.leave-org-confirm.accept"
|
||||
msgid "modals.leave-organization-confirm.accept"
|
||||
msgstr "Leave organization"
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:208
|
||||
msgid "modals.leave-org-confirm.message"
|
||||
msgid "modals.leave-organization-confirm.message"
|
||||
msgstr ""
|
||||
"You will permanently lose access to all teams, projects, and files within "
|
||||
"it."
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:207
|
||||
msgid "modals.leave-org-confirm.title"
|
||||
msgid "modals.leave-organization-confirm.title"
|
||||
msgstr "Leaving %s organization?"
|
||||
|
||||
#: src/app/main/ui/delete_shared.cljs:54
|
||||
@ -4272,21 +4272,21 @@ msgid "modals.remove-shared-confirm.message"
|
||||
msgstr "Remove “%s” as Shared Library"
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:256
|
||||
msgid "modals.remove-team-org.accept"
|
||||
msgid "modals.remove-team-organization.accept"
|
||||
msgstr "Remove from organization"
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:254
|
||||
msgid "modals.remove-team-org.info"
|
||||
msgid "modals.remove-team-organization.info"
|
||||
msgstr ""
|
||||
"Projects and files will remain available to team members, but the "
|
||||
"organization's settings will no longer apply."
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:253
|
||||
msgid "modals.remove-team-org.text"
|
||||
msgid "modals.remove-team-organization.text"
|
||||
msgstr "Are you sure you want to remove the '%s' team from the '%s' organization?"
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:252
|
||||
msgid "modals.remove-team-org.title"
|
||||
msgid "modals.remove-team-organization.title"
|
||||
msgstr "REMOVE TEAM FROM THE ORGANIZATION"
|
||||
|
||||
#: src/app/main/ui/workspace/nudge.cljs:52
|
||||
@ -4354,7 +4354,7 @@ msgid "nitrate.activation-success.active-until"
|
||||
msgstr "Your plan is active until %s."
|
||||
|
||||
#: src/app/main/ui/dashboard/subscription.cljs:195, src/app/main/ui/nitrate/nitrate_activation_success_modal.cljs:68
|
||||
msgid "nitrate.activation-success.create-org"
|
||||
msgid "nitrate.activation-success.create-organization"
|
||||
msgstr "Create organization"
|
||||
|
||||
#: src/app/main/ui/nitrate/nitrate_activation_success_modal.cljs:63
|
||||
@ -4409,7 +4409,7 @@ msgstr ""
|
||||
"To switch to this plan, please contact our sales team. We'll help you "
|
||||
"update your subscription and ensure everything is set up correctly."
|
||||
|
||||
msgid "nitrate.contact-sales.downgrade-org-deleted"
|
||||
msgid "nitrate.contact-sales.downgrade-organization-deleted"
|
||||
msgstr "Your organization will be deleted."
|
||||
|
||||
msgid "nitrate.contact-sales.downgrade-storage-limited"
|
||||
@ -5863,7 +5863,7 @@ msgid "import-shortcuts.apply"
|
||||
msgstr "Apply"
|
||||
|
||||
#: src/app/main/ui/dashboard/subscription.cljs:191
|
||||
msgid "subscription.banner.create-org-info"
|
||||
msgid "subscription.banner.create-organization-info"
|
||||
msgstr ""
|
||||
"Create an organization and see exactly how Enterprise works. Set rules, "
|
||||
"manage your teams and discover a new level of control over your design "
|
||||
@ -6098,7 +6098,7 @@ msgstr "Flat monthly bill"
|
||||
msgid "subscription.settings.enterprise.unlimited-storage-benefit"
|
||||
msgstr "Unlimited storage"
|
||||
|
||||
msgid "subscription.settings.enterprise.nitrate.multi-org-management"
|
||||
msgid "subscription.settings.enterprise.nitrate.multi-organization-management"
|
||||
msgstr "Multi-Org Management:"
|
||||
|
||||
msgid "subscription.settings.enterprise.nitrate.support-team"
|
||||
|
||||
@ -102,7 +102,7 @@ msgid "auth.new-password"
|
||||
msgstr "Introduce la nueva contraseña"
|
||||
|
||||
#: src/app/main/ui/auth/verify_token.cljs:53
|
||||
msgid "auth.notifications.org-invitation-accepted"
|
||||
msgid "auth.notifications.organization-invitation-accepted"
|
||||
msgstr "Te uniste a la organización %s"
|
||||
|
||||
#: src/app/main/ui/auth/recovery.cljs:36
|
||||
@ -304,7 +304,7 @@ msgid "common.unpublish"
|
||||
msgstr "Despublicar"
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:165
|
||||
msgid "dasboard.leave-org.toast"
|
||||
msgid "dashboard.leave-organization.toast"
|
||||
msgstr "Ya no eres miembro de la organización %s."
|
||||
|
||||
#: src/app/main/ui/dashboard/projects.cljs:90
|
||||
@ -374,25 +374,25 @@ msgid "dashboard.change-email"
|
||||
msgstr "Cambiar correo"
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.change-org-modal.accept"
|
||||
msgid "dashboard.change-organization-modal.accept"
|
||||
msgstr "MOVER EL EQUIPO"
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.change-org-modal.choose"
|
||||
msgid "dashboard.change-organization-modal.choose"
|
||||
msgstr "Mover a:"
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.change-org-modal.select"
|
||||
msgid "dashboard.change-organization-modal.select"
|
||||
msgstr "Elige una organización"
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.change-org-modal.text"
|
||||
msgid "dashboard.change-organization-modal.text"
|
||||
msgstr ""
|
||||
"Los miembros del equipo seguirán teniendo acceso a los proyectos y "
|
||||
"ficheros. El equipo tendrá la configuración de la nueva organización."
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:399, src/app/main/ui/dashboard/team_form.cljs:157
|
||||
msgid "dashboard.change-org-modal.title"
|
||||
msgid "dashboard.change-organization-modal.title"
|
||||
msgstr "Cambiar el equipo de organización"
|
||||
|
||||
#: src/app/main/ui/dashboard/deleted.cljs:316
|
||||
@ -404,7 +404,7 @@ msgid "dashboard.copy-suffix"
|
||||
msgstr "(copia)"
|
||||
|
||||
#: src/app/main/ui/dashboard/sidebar.cljs:371
|
||||
msgid "dashboard.create-new-org"
|
||||
msgid "dashboard.create-new-organization"
|
||||
msgstr "Crear org"
|
||||
|
||||
#: src/app/main/ui/dashboard/sidebar.cljs:439
|
||||
@ -824,7 +824,7 @@ msgid "dashboard.invite-profile"
|
||||
msgstr "Invitar a la gente"
|
||||
|
||||
#: src/app/main/ui/dashboard/sidebar.cljs:671
|
||||
msgid "dashboard.leave-org"
|
||||
msgid "dashboard.leave-organization"
|
||||
msgstr "Abandonar organización"
|
||||
|
||||
#: src/app/main/ui/dashboard/sidebar.cljs:570, src/app/main/ui/dashboard/sidebar.cljs:577, src/app/main/ui/dashboard/sidebar.cljs:582, src/app/main/ui/dashboard/team.cljs:433
|
||||
@ -900,7 +900,7 @@ msgid "dashboard.no-matches-for"
|
||||
msgstr "No se encuentra “%s“"
|
||||
|
||||
#: src/app/main/ui/dashboard/team_form.cljs:156
|
||||
msgid "dashboard.no-org-allows-create-team.message"
|
||||
msgid "dashboard.no-organization-allows-create-team.message"
|
||||
msgstr "No tienes permiso para añadir equipos a ninguna de tus organizaciones."
|
||||
|
||||
#: src/app/main/ui/dashboard/team_form.cljs:152
|
||||
@ -972,7 +972,7 @@ msgid "dashboard.order-invitations-by-status"
|
||||
msgstr "Ordenar por estado"
|
||||
|
||||
#: src/app/main/data/dashboard.cljs:753
|
||||
msgid "dashboard.org-deleted"
|
||||
msgid "dashboard.organization-deleted"
|
||||
msgstr "La organización %s se ha borrado."
|
||||
|
||||
#: src/app/main/ui/settings/password.cljs:96, src/app/main/ui/settings/password.cljs:109
|
||||
@ -1012,7 +1012,7 @@ msgid "dashboard.plugins.try-plugin"
|
||||
msgstr "Prueba la extensión: "
|
||||
|
||||
#: src/app/main/ui/dashboard/sidebar.cljs:798
|
||||
msgid "dashboard.plus-create-new-org"
|
||||
msgid "dashboard.plus-create-new-organization"
|
||||
msgstr "+ Crear org"
|
||||
|
||||
#: src/app/main/data/dashboard.cljs:805
|
||||
@ -1100,36 +1100,36 @@ msgid "dashboard.section.teams"
|
||||
msgstr "Equipos"
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.select-org-modal.accept"
|
||||
msgid "dashboard.select-organization-modal.accept"
|
||||
msgstr "AÑADIR A UNA ORGANIZACIÓN"
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.select-org-modal.choose"
|
||||
msgid "dashboard.select-organization-modal.choose"
|
||||
msgstr "Elige una organización:"
|
||||
|
||||
#: src/app/main/ui/dashboard/team.cljs:975
|
||||
msgid "dashboard.select-org-modal.external-invitations-warning"
|
||||
msgid "dashboard.select-organization-modal.external-invitations-warning"
|
||||
msgstr ""
|
||||
"\"%s\" solo permite que los miembros de la organización se unan a sus "
|
||||
"equipos. Cualquier invitación pendiente a personas fuera de la organización "
|
||||
"será revocada."
|
||||
|
||||
#: src/app/main/ui/dashboard/team.cljs:971
|
||||
msgid "dashboard.select-org-modal.external-invitations-will-be-canceled"
|
||||
msgid "dashboard.select-organization-modal.external-invitations-will-be-canceled"
|
||||
msgstr "Las invitaciones pendientes a usuarios externos serán canceladas."
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.select-org-modal.permission-info"
|
||||
msgid "dashboard.select-organization-modal.permission-info"
|
||||
msgstr ""
|
||||
"Aquí encontrarás todas las organizaciones en las que tienes permiso para "
|
||||
"crear o añadir equipos."
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.select-org-modal.select"
|
||||
msgid "dashboard.select-organization-modal.select"
|
||||
msgstr "Elige una organización"
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:342, src/app/main/ui/dashboard/team_form.cljs:155
|
||||
msgid "dashboard.select-org-modal.title"
|
||||
msgid "dashboard.select-organization-modal.title"
|
||||
msgstr "Añadir el equipo a una organización"
|
||||
|
||||
#: src/app/main/ui/settings/options.cljs:66
|
||||
@ -1253,7 +1253,7 @@ msgid "dashboard.success-move-project"
|
||||
msgstr "Tu proyecto ha sido movido con éxito"
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.team-belong-org"
|
||||
msgid "dashboard.team-belong-organization"
|
||||
msgstr "Este equipo ahora pertenece a la organización %s"
|
||||
|
||||
#: src/app/main/ui/dashboard/team.cljs:1602
|
||||
@ -1265,7 +1265,7 @@ msgid "dashboard.team-members"
|
||||
msgstr "Integrantes del equipo"
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.team-no-longer-belong-org"
|
||||
msgid "dashboard.team-no-longer-belong-organization"
|
||||
msgstr "Este equipo ya no pertenece a la organización %s"
|
||||
|
||||
#: src/app/main/ui/dashboard/team.cljs:1609
|
||||
@ -1277,7 +1277,7 @@ msgid "dashboard.team-organization.add"
|
||||
msgstr "Añadir a una organización"
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:341, src/app/main/data/nitrate.cljs:398
|
||||
msgid "dashboard.team-organization.add.no-valid-orgs"
|
||||
msgid "dashboard.team-organization.add.no-valid-organizations"
|
||||
msgstr "No tienes permiso para añadir este equipo a ninguna de tus organizaciones."
|
||||
|
||||
#: src/app/main/ui/dashboard/team.cljs:1632
|
||||
@ -1285,7 +1285,7 @@ msgid "dashboard.team-organization.change"
|
||||
msgstr "Cambiar el equipo de organización"
|
||||
|
||||
#: src/app/main/ui/dashboard/team.cljs:905
|
||||
msgid "dashboard.team-organization.disabled-org-tooltip"
|
||||
msgid "dashboard.team-organization.disabled-organization-tooltip"
|
||||
msgstr "Solo se puede invitar a personas que estén dentro de tu organización."
|
||||
|
||||
#: src/app/main/ui/dashboard/team.cljs:1639
|
||||
@ -1351,7 +1351,7 @@ msgid "dashboard.upgrade-plan.penpot-free"
|
||||
msgstr "Penpot Gratis"
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.user-no-longer-belong-org"
|
||||
msgid "dashboard.user-no-longer-belong-organization"
|
||||
msgstr "Ya no perteneces a la organización %s"
|
||||
|
||||
#: src/app/main/ui/settings/options.cljs:109
|
||||
@ -1693,17 +1693,17 @@ msgid "errors.migration-in-progress"
|
||||
msgstr "Migración en proceso"
|
||||
|
||||
#, unused
|
||||
msgid "errors.org-leave.no-valid-teams"
|
||||
msgid "errors.organization-leave.no-valid-teams"
|
||||
msgstr ""
|
||||
"Ha habido un problema abandonando la organización. Intentalo de nuevo, por "
|
||||
"favor."
|
||||
|
||||
#, unused
|
||||
msgid "errors.org-leave.org-owner-cannot-leave"
|
||||
msgid "errors.organization-leave.organization-owner-cannot-leave"
|
||||
msgstr "El dueño de la organización no puede abandonarla."
|
||||
|
||||
#: src/app/main/ui/auth/verify_token.cljs:97
|
||||
msgid "errors.org-not-found"
|
||||
msgid "errors.organization-not-found"
|
||||
msgstr "Esa organización no existe"
|
||||
|
||||
#: src/app/main/ui/settings/password.cljs
|
||||
@ -3655,17 +3655,17 @@ msgid "modals.add-shared-confirm.message"
|
||||
msgstr "Añadir “%s” como Biblioteca Compartida"
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:199
|
||||
msgid "modals.before-leave-org.message"
|
||||
msgid "modals.before-leave-organization.message"
|
||||
msgstr ""
|
||||
"Eres el único miembro de algunos equipos. Esos equipos se van a borrar, "
|
||||
"junto con sus proyectos y ficheros."
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:198, src/app/main/ui/dashboard/change_owner.cljs:151
|
||||
msgid "modals.before-leave-org.title"
|
||||
msgid "modals.before-leave-organization.title"
|
||||
msgstr "ANTES DE ABANDONAR LA ORGANIZACIÓN"
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:202
|
||||
msgid "modals.before-leave-org.warning"
|
||||
msgid "modals.before-leave-organization.warning"
|
||||
msgstr "Se van a borrar todos los equipos en los que eres el único miembro."
|
||||
|
||||
#: src/app/main/ui/workspace/nudge.cljs:59
|
||||
@ -3723,22 +3723,22 @@ msgstr "Si borras tu cuenta perderás todos tus proyectos y archivos."
|
||||
|
||||
#: src/app/main/ui/settings/delete_account.cljs
|
||||
#, unused
|
||||
msgid "modals.delete-account.info.with-orgs"
|
||||
msgid "modals.delete-account.info.with-organizations"
|
||||
msgstr ""
|
||||
"Esta acción es irreversible. Tu cuenta, suscripción y organizaciones se "
|
||||
"eliminarán inmediatamente. También perderás el acceso a tus proyectos y "
|
||||
"archivos actuales."
|
||||
|
||||
#: src/app/main/ui/settings/delete_account.cljs:83
|
||||
msgid "modals.delete-account.owned-orgs.list-title"
|
||||
msgid "modals.delete-account.owned-organizations.list-title"
|
||||
msgstr "Organizaciones que se eliminarán"
|
||||
|
||||
#: src/app/main/ui/settings/delete_account.cljs:98
|
||||
msgid "modals.delete-account.owned-orgs.members-count"
|
||||
msgid "modals.delete-account.owned-organizations.members-count"
|
||||
msgstr "%s miembros"
|
||||
|
||||
#: src/app/main/ui/settings/delete_account.cljs:96
|
||||
msgid "modals.delete-account.owned-orgs.teams-count"
|
||||
msgid "modals.delete-account.owned-organizations.teams-count"
|
||||
msgstr "%s equipos"
|
||||
|
||||
#: src/app/main/ui/settings/delete_account.cljs:64
|
||||
@ -3812,7 +3812,7 @@ msgid "modals.delete-font.title"
|
||||
msgstr "Eliminando fuente"
|
||||
|
||||
#: src/app/main/data/team.cljs:129
|
||||
msgid "modals.delete-org-team-confirm.message"
|
||||
msgid "modals.delete-organization-team-confirm.message"
|
||||
msgstr ""
|
||||
"¿Estás seguro de que deseas eliminar este equipo que forma parte de la "
|
||||
"organización %s?"
|
||||
@ -3957,7 +3957,7 @@ msgid "modals.invite-restricted-members.all-blocked-title"
|
||||
msgstr "No se envió ninguna invitación"
|
||||
|
||||
#: src/app/main/data/team.cljs:162
|
||||
msgid "modals.invite-restricted-members.all-org-members-in-team"
|
||||
msgid "modals.invite-restricted-members.all-organization-members-in-team"
|
||||
msgstr ""
|
||||
"Solo se puede invitar a este equipo a miembros de la organización \"%s\", y "
|
||||
"todas esas personas ya son miembros del equipo. Si necesitas más "
|
||||
@ -4044,35 +4044,35 @@ msgid "modals.leave-confirm.title"
|
||||
msgstr "Abandonando el equipo"
|
||||
|
||||
#: src/app/main/ui/dashboard/change_owner.cljs:158
|
||||
msgid "modals.leave-org-and-reassign.hint"
|
||||
msgid "modals.leave-organization-and-reassign.hint"
|
||||
msgstr ""
|
||||
"Tienes la propiedad de algunos equipos de esta organización. Por favor "
|
||||
"selecciona otra persona integrante para promover al rol Propiedad."
|
||||
|
||||
#: src/app/main/ui/dashboard/change_owner.cljs:161
|
||||
msgid "modals.leave-org-and-reassign.hint-delete"
|
||||
msgid "modals.leave-organization-and-reassign.hint-delete"
|
||||
msgstr ""
|
||||
"Eres el único miembro de algunos equipos. Esos equipos se van a borrar, "
|
||||
"junto con sus proyectos y ficheros."
|
||||
|
||||
#: src/app/main/ui/dashboard/change_owner.cljs:163
|
||||
msgid "modals.leave-org-and-reassign.hint-promote"
|
||||
msgid "modals.leave-organization-and-reassign.hint-promote"
|
||||
msgstr ""
|
||||
"Además, tienes la propiedad de algunos equipos de esta organización. Por "
|
||||
"favor selecciona otra persona integrante para promover al rol Propiedad."
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:200, src/app/main/data/nitrate.cljs:209
|
||||
msgid "modals.leave-org-confirm.accept"
|
||||
msgid "modals.leave-organization-confirm.accept"
|
||||
msgstr "Abandonar organización"
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:208
|
||||
msgid "modals.leave-org-confirm.message"
|
||||
msgid "modals.leave-organization-confirm.message"
|
||||
msgstr ""
|
||||
"Perderás permanentemente el acceso a todos los equipos, proyectos y "
|
||||
"archivos en ella."
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:207
|
||||
msgid "modals.leave-org-confirm.title"
|
||||
msgid "modals.leave-organization-confirm.title"
|
||||
msgstr "¿Abandonando la organización %s?"
|
||||
|
||||
#: src/app/main/ui/delete_shared.cljs:54
|
||||
@ -4147,21 +4147,21 @@ msgid "modals.remove-shared-confirm.message"
|
||||
msgstr "Añadir “%s” como Biblioteca Compartida"
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:256
|
||||
msgid "modals.remove-team-org.accept"
|
||||
msgid "modals.remove-team-organization.accept"
|
||||
msgstr "Eliminar de la organización"
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:254
|
||||
msgid "modals.remove-team-org.info"
|
||||
msgid "modals.remove-team-organization.info"
|
||||
msgstr ""
|
||||
"Los proyectos y archivos seguirán estando disponibles para los miembros del "
|
||||
"equipo, pero la configuración de la organización dejará de aplicarse."
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:253
|
||||
msgid "modals.remove-team-org.text"
|
||||
msgid "modals.remove-team-organization.text"
|
||||
msgstr "¿Estás seguro de que quieres eliminar el equipo %s de la organización %s?"
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:252
|
||||
msgid "modals.remove-team-org.title"
|
||||
msgid "modals.remove-team-organization.title"
|
||||
msgstr "ELIMINAR EQUIPO DE LA ORGANIZACIÓN"
|
||||
|
||||
#: src/app/main/ui/workspace/nudge.cljs:52
|
||||
@ -4229,7 +4229,7 @@ msgid "nitrate.activation-success.active-until"
|
||||
msgstr "Tu plan está activo hasta el %s."
|
||||
|
||||
#: src/app/main/ui/dashboard/subscription.cljs:195, src/app/main/ui/nitrate/nitrate_activation_success_modal.cljs:68
|
||||
msgid "nitrate.activation-success.create-org"
|
||||
msgid "nitrate.activation-success.create-organization"
|
||||
msgstr "Crear organización"
|
||||
|
||||
#: src/app/main/ui/nitrate/nitrate_activation_success_modal.cljs:63
|
||||
@ -4285,7 +4285,7 @@ msgstr ""
|
||||
"ayudaremos a actualizar tu suscripción y a asegurarnos de que todo esté "
|
||||
"configurado correctamente."
|
||||
|
||||
msgid "nitrate.contact-sales.downgrade-org-deleted"
|
||||
msgid "nitrate.contact-sales.downgrade-organization-deleted"
|
||||
msgstr "Tu organización será eliminada."
|
||||
|
||||
msgid "nitrate.contact-sales.downgrade-storage-limited"
|
||||
@ -5718,7 +5718,7 @@ msgid "import-shortcuts.apply"
|
||||
msgstr "Aplicar"
|
||||
|
||||
#: src/app/main/ui/dashboard/subscription.cljs:191
|
||||
msgid "subscription.banner.create-org-info"
|
||||
msgid "subscription.banner.create-organization-info"
|
||||
msgstr ""
|
||||
"Crea una organización y descubre exactamente cómo funciona Enterprise. "
|
||||
"Establece normas, gestiona tus equipos y disfruta de un nuevo nivel de "
|
||||
@ -5962,7 +5962,7 @@ msgstr "Factura mensual fija"
|
||||
msgid "subscription.settings.enterprise.unlimited-storage-benefit"
|
||||
msgstr "Almacenamiento ilimitado"
|
||||
|
||||
msgid "subscription.settings.enterprise.nitrate.multi-org-management"
|
||||
msgid "subscription.settings.enterprise.nitrate.multi-organization-management"
|
||||
msgstr "Gestión multiorganización:"
|
||||
|
||||
msgid "subscription.settings.enterprise.nitrate.support-team"
|
||||
|
||||
@ -366,7 +366,7 @@ msgid "dashboard.copy-suffix"
|
||||
msgstr "(copie)"
|
||||
|
||||
#: src/app/main/ui/dashboard/sidebar.cljs:371
|
||||
msgid "dashboard.create-new-org"
|
||||
msgid "dashboard.create-new-organization"
|
||||
msgstr "Créer une organisation"
|
||||
|
||||
#: src/app/main/ui/dashboard/sidebar.cljs:439
|
||||
|
||||
@ -98,7 +98,7 @@ msgid "auth.new-password"
|
||||
msgstr "Nouveau mot de passe"
|
||||
|
||||
#: src/app/main/ui/auth/verify_token.cljs:53
|
||||
msgid "auth.notifications.org-invitation-accepted"
|
||||
msgid "auth.notifications.organization-invitation-accepted"
|
||||
msgstr "Tu fais maintenant partie de %s"
|
||||
|
||||
#: src/app/main/ui/auth/recovery.cljs:36
|
||||
@ -300,7 +300,7 @@ msgid "common.unpublish"
|
||||
msgstr "Dépublier"
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:165
|
||||
msgid "dasboard.leave-org.toast"
|
||||
msgid "dashboard.leave-organization.toast"
|
||||
msgstr "Tu ne fais plus partie de l'organisation %s."
|
||||
|
||||
#: src/app/main/ui/dashboard/projects.cljs:90
|
||||
@ -368,25 +368,25 @@ msgid "dashboard.change-email"
|
||||
msgstr "Changer l'adresse courriel"
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.change-org-modal.accept"
|
||||
msgid "dashboard.change-organization-modal.accept"
|
||||
msgstr "DÉPLACER L'ÉQUIPE"
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.change-org-modal.choose"
|
||||
msgid "dashboard.change-organization-modal.choose"
|
||||
msgstr "Déplacer vers :"
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.change-org-modal.select"
|
||||
msgid "dashboard.change-organization-modal.select"
|
||||
msgstr "Sélectionner une organisation"
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.change-org-modal.text"
|
||||
msgid "dashboard.change-organization-modal.text"
|
||||
msgstr ""
|
||||
"Les membres de l'équipe maintiendront l'accès aux projets et fichiers. "
|
||||
"L'équipe recevra les configurations de la nouvelle organisation."
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:399, src/app/main/ui/dashboard/team_form.cljs:157
|
||||
msgid "dashboard.change-org-modal.title"
|
||||
msgid "dashboard.change-organization-modal.title"
|
||||
msgstr "Changer l'organisation de l'équipe"
|
||||
|
||||
#: src/app/main/ui/dashboard/deleted.cljs:316
|
||||
@ -398,7 +398,7 @@ msgid "dashboard.copy-suffix"
|
||||
msgstr "(copier)"
|
||||
|
||||
#: src/app/main/ui/dashboard/sidebar.cljs:371
|
||||
msgid "dashboard.create-new-org"
|
||||
msgid "dashboard.create-new-organization"
|
||||
msgstr "Crée une nouvelle organisation"
|
||||
|
||||
#: src/app/main/ui/dashboard/sidebar.cljs:439
|
||||
@ -822,7 +822,7 @@ msgid "dashboard.invite-profile"
|
||||
msgstr "Inviter des gens"
|
||||
|
||||
#: src/app/main/ui/dashboard/sidebar.cljs:671
|
||||
msgid "dashboard.leave-org"
|
||||
msgid "dashboard.leave-organization"
|
||||
msgstr "Quitter l'organisation"
|
||||
|
||||
#: src/app/main/ui/dashboard/sidebar.cljs:570, src/app/main/ui/dashboard/sidebar.cljs:577, src/app/main/ui/dashboard/sidebar.cljs:582, src/app/main/ui/dashboard/team.cljs:433
|
||||
@ -900,7 +900,7 @@ msgid "dashboard.no-matches-for"
|
||||
msgstr "Aucun résultat de recherche pour « %s »"
|
||||
|
||||
#: src/app/main/ui/dashboard/team_form.cljs:156
|
||||
msgid "dashboard.no-org-allows-create-team.message"
|
||||
msgid "dashboard.no-organization-allows-create-team.message"
|
||||
msgstr "Tu n'as pas la permission d'ajouter des équipes à tes organisations."
|
||||
|
||||
#: src/app/main/ui/dashboard/team_form.cljs:152
|
||||
@ -970,7 +970,7 @@ msgid "dashboard.order-invitations-by-status"
|
||||
msgstr "Ordre par statut"
|
||||
|
||||
#: src/app/main/data/dashboard.cljs:753
|
||||
msgid "dashboard.org-deleted"
|
||||
msgid "dashboard.organization-deleted"
|
||||
msgstr "L'organisation %s a été supprimée."
|
||||
|
||||
#: src/app/main/ui/settings/password.cljs:96, src/app/main/ui/settings/password.cljs:109
|
||||
@ -1010,7 +1010,7 @@ msgid "dashboard.plugins.try-plugin"
|
||||
msgstr "Essayer le plugiciel : "
|
||||
|
||||
#: src/app/main/ui/dashboard/sidebar.cljs:798
|
||||
msgid "dashboard.plus-create-new-org"
|
||||
msgid "dashboard.plus-create-new-organization"
|
||||
msgstr "+ Créer une org"
|
||||
|
||||
#: src/app/main/data/dashboard.cljs:805
|
||||
@ -1095,36 +1095,36 @@ msgid "dashboard.section.organizations"
|
||||
msgstr "Organisations"
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.select-org-modal.accept"
|
||||
msgid "dashboard.select-organization-modal.accept"
|
||||
msgstr "AJOUTER À L'ORGANISATION"
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.select-org-modal.choose"
|
||||
msgid "dashboard.select-organization-modal.choose"
|
||||
msgstr "Choisir une organisation :"
|
||||
|
||||
#: src/app/main/ui/dashboard/team.cljs:975
|
||||
msgid "dashboard.select-org-modal.external-invitations-warning"
|
||||
msgid "dashboard.select-organization-modal.external-invitations-warning"
|
||||
msgstr ""
|
||||
"« %s » ne permet qu'aux membres d'organisations de joindre ses équipes. Les "
|
||||
"invitations en attente à des gens externes à l'organisation seront "
|
||||
"révoquées."
|
||||
|
||||
#: src/app/main/ui/dashboard/team.cljs:971
|
||||
msgid "dashboard.select-org-modal.external-invitations-will-be-canceled"
|
||||
msgid "dashboard.select-organization-modal.external-invitations-will-be-canceled"
|
||||
msgstr "Les invitations en attente à des usagers externes seront annulées."
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.select-org-modal.permission-info"
|
||||
msgid "dashboard.select-organization-modal.permission-info"
|
||||
msgstr ""
|
||||
"Ici tu trouveras toutes les organisations auxquelles tu as le droit de "
|
||||
"créer ou ajouter des équipes."
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.select-org-modal.select"
|
||||
msgid "dashboard.select-organization-modal.select"
|
||||
msgstr "Choisir une organisation :"
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:342, src/app/main/ui/dashboard/team_form.cljs:155
|
||||
msgid "dashboard.select-org-modal.title"
|
||||
msgid "dashboard.select-organization-modal.title"
|
||||
msgstr "Ajouter une équipe à l'organisation"
|
||||
|
||||
#: src/app/main/ui/settings/options.cljs:66
|
||||
@ -1248,7 +1248,7 @@ msgid "dashboard.success-move-project"
|
||||
msgstr "Le projet a été déplacé avec succès"
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.team-belong-org"
|
||||
msgid "dashboard.team-belong-organization"
|
||||
msgstr "Cette équipe appartient maintenant à l'organisation %s"
|
||||
|
||||
#: src/app/main/ui/dashboard/team.cljs:1602
|
||||
@ -1260,7 +1260,7 @@ msgid "dashboard.team-members"
|
||||
msgstr "Membres de l'équipe"
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.team-no-longer-belong-org"
|
||||
msgid "dashboard.team-no-longer-belong-organization"
|
||||
msgstr "Cette équipe n'appartient plus à l'organisation %s"
|
||||
|
||||
#: src/app/main/ui/dashboard/team.cljs:1609
|
||||
@ -1272,7 +1272,7 @@ msgid "dashboard.team-organization.add"
|
||||
msgstr "Ajouter à l'organisation"
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:341, src/app/main/data/nitrate.cljs:398
|
||||
msgid "dashboard.team-organization.add.no-valid-orgs"
|
||||
msgid "dashboard.team-organization.add.no-valid-organizations"
|
||||
msgstr "Tu n'as pas le droit d'ajouter cette équipe à tes organisations."
|
||||
|
||||
#: src/app/main/ui/dashboard/team.cljs:1632
|
||||
@ -1280,7 +1280,7 @@ msgid "dashboard.team-organization.change"
|
||||
msgstr "Changer l'organisation de l'équipe"
|
||||
|
||||
#: src/app/main/ui/dashboard/team.cljs:905
|
||||
msgid "dashboard.team-organization.disabled-org-tooltip"
|
||||
msgid "dashboard.team-organization.disabled-organization-tooltip"
|
||||
msgstr "Seules les personnes de ton organisation peuvent être invitées."
|
||||
|
||||
#: src/app/main/ui/dashboard/team.cljs:1639
|
||||
@ -1346,7 +1346,7 @@ msgid "dashboard.upgrade-plan.penpot-free"
|
||||
msgstr "Penpot gratuit"
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.user-no-longer-belong-org"
|
||||
msgid "dashboard.user-no-longer-belong-organization"
|
||||
msgstr "Tu n'es plus membre de l'organisation %s"
|
||||
|
||||
#: src/app/main/ui/settings/options.cljs:109
|
||||
@ -1726,15 +1726,15 @@ msgid "errors.only-creator-can-unlock"
|
||||
msgstr "Seul le créateur de la version peut la déverrouiller"
|
||||
|
||||
#, unused
|
||||
msgid "errors.org-leave.no-valid-teams"
|
||||
msgid "errors.organization-leave.no-valid-teams"
|
||||
msgstr "Un problème est survenu en quittant l'organisation. Merci de réessayer."
|
||||
|
||||
#, unused
|
||||
msgid "errors.org-leave.org-owner-cannot-leave"
|
||||
msgid "errors.organization-leave.organization-owner-cannot-leave"
|
||||
msgstr "Le propriétaire de l'organisation ne peut pas la quitter."
|
||||
|
||||
#: src/app/main/ui/auth/verify_token.cljs:97
|
||||
msgid "errors.org-not-found"
|
||||
msgid "errors.organization-not-found"
|
||||
msgstr "Cette organisation n'existe pas"
|
||||
|
||||
#: src/app/main/ui/settings/password.cljs
|
||||
@ -3760,17 +3760,17 @@ msgid "modals.add-shared-confirm.message"
|
||||
msgstr "Ajouter « %s » comme bibliothèque partagée"
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:199
|
||||
msgid "modals.before-leave-org.message"
|
||||
msgid "modals.before-leave-organization.message"
|
||||
msgstr ""
|
||||
"Tu es le seul membre de certaines équipes. Ces équipes, ainsi que leurs "
|
||||
"projets et fichiers, seront supprimées."
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:198, src/app/main/ui/dashboard/change_owner.cljs:151
|
||||
msgid "modals.before-leave-org.title"
|
||||
msgid "modals.before-leave-organization.title"
|
||||
msgstr "AVANT DE QUITTER L'ORGANISATION"
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:202
|
||||
msgid "modals.before-leave-org.warning"
|
||||
msgid "modals.before-leave-organization.warning"
|
||||
msgstr "Toute équipe dont tu es le seul membre sera supprimée."
|
||||
|
||||
#: src/app/main/ui/workspace/nudge.cljs:59
|
||||
@ -3830,22 +3830,22 @@ msgstr "Si tu supprimes ton compte, tu vas perdre tous tes projets et fichiers."
|
||||
|
||||
#: src/app/main/ui/settings/delete_account.cljs
|
||||
#, unused
|
||||
msgid "modals.delete-account.info.with-orgs"
|
||||
msgid "modals.delete-account.info.with-organizations"
|
||||
msgstr ""
|
||||
"Cette action est irréversible. Ton compte, abonnement et organisations "
|
||||
"seront supprimés immédiatement. L'accès aux projets et fichiers courants "
|
||||
"sera perdu."
|
||||
|
||||
#: src/app/main/ui/settings/delete_account.cljs:83
|
||||
msgid "modals.delete-account.owned-orgs.list-title"
|
||||
msgid "modals.delete-account.owned-organizations.list-title"
|
||||
msgstr "Organisations qui seront supprimées"
|
||||
|
||||
#: src/app/main/ui/settings/delete_account.cljs:98
|
||||
msgid "modals.delete-account.owned-orgs.members-count"
|
||||
msgid "modals.delete-account.owned-organizations.members-count"
|
||||
msgstr "%s membres"
|
||||
|
||||
#: src/app/main/ui/settings/delete_account.cljs:96
|
||||
msgid "modals.delete-account.owned-orgs.teams-count"
|
||||
msgid "modals.delete-account.owned-organizations.teams-count"
|
||||
msgstr "%s équipes"
|
||||
|
||||
#: src/app/main/ui/settings/delete_account.cljs:64
|
||||
@ -3929,7 +3929,7 @@ msgid "modals.delete-font.title"
|
||||
msgstr "Suppression de la police"
|
||||
|
||||
#: src/app/main/data/team.cljs:129
|
||||
msgid "modals.delete-org-team-confirm.message"
|
||||
msgid "modals.delete-organization-team-confirm.message"
|
||||
msgstr "Vraiment supprimer cette équipe qui fait partie de l'organisation %s?"
|
||||
|
||||
#: src/app/main/ui/workspace/context_menu.cljs:707, src/app/main/ui/workspace/sidebar/sitemap.cljs:109
|
||||
@ -4077,7 +4077,7 @@ msgid "modals.invite-restricted-members.all-blocked-title"
|
||||
msgstr "Aucune invitation envoyée"
|
||||
|
||||
#: src/app/main/data/team.cljs:162
|
||||
msgid "modals.invite-restricted-members.all-org-members-in-team"
|
||||
msgid "modals.invite-restricted-members.all-organization-members-in-team"
|
||||
msgstr ""
|
||||
"Seuls les membres de l'organisation « %s » peuvent être invités à cette "
|
||||
"équipe et ils sont tous déjà membres. Pour plus d'informations, contacter "
|
||||
@ -4164,35 +4164,35 @@ msgid "modals.leave-confirm.title"
|
||||
msgstr "Quitter l'équipe"
|
||||
|
||||
#: src/app/main/ui/dashboard/change_owner.cljs:158
|
||||
msgid "modals.leave-org-and-reassign.hint"
|
||||
msgid "modals.leave-organization-and-reassign.hint"
|
||||
msgstr ""
|
||||
"Tu es propriétaire de certaines équipes de cette organisation. Sélectionner "
|
||||
"un autre membre afin de les promouvoir au rôle de propriétaire."
|
||||
|
||||
#: src/app/main/ui/dashboard/change_owner.cljs:161
|
||||
msgid "modals.leave-org-and-reassign.hint-delete"
|
||||
msgid "modals.leave-organization-and-reassign.hint-delete"
|
||||
msgstr ""
|
||||
"Tu es membre unique de certaines équipes. Ces équipes ainsi que leurs "
|
||||
"projets et fichiers seront supprimés."
|
||||
|
||||
#: src/app/main/ui/dashboard/change_owner.cljs:163
|
||||
msgid "modals.leave-org-and-reassign.hint-promote"
|
||||
msgid "modals.leave-organization-and-reassign.hint-promote"
|
||||
msgstr ""
|
||||
"De plus, tu es propriétaire de certaines équipes de cette organisation. "
|
||||
"Sélectionner un autre membre afin de les promouvoir au rôle de propriétaire."
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:200, src/app/main/data/nitrate.cljs:209
|
||||
msgid "modals.leave-org-confirm.accept"
|
||||
msgid "modals.leave-organization-confirm.accept"
|
||||
msgstr "Quitter l'organisation"
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:208
|
||||
msgid "modals.leave-org-confirm.message"
|
||||
msgid "modals.leave-organization-confirm.message"
|
||||
msgstr ""
|
||||
"Tu perdras accès en permanence à toutes les équipes, leurs projets et "
|
||||
"fichiers."
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:207
|
||||
msgid "modals.leave-org-confirm.title"
|
||||
msgid "modals.leave-organization-confirm.title"
|
||||
msgstr "Quitter l'organisation %s?"
|
||||
|
||||
#: src/app/main/ui/delete_shared.cljs:54
|
||||
@ -4267,21 +4267,21 @@ msgid "modals.remove-shared-confirm.message"
|
||||
msgstr "Retirer « %s » comme bibliothèque partagée"
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:256
|
||||
msgid "modals.remove-team-org.accept"
|
||||
msgid "modals.remove-team-organization.accept"
|
||||
msgstr "Retirer de l'organisation"
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:254
|
||||
msgid "modals.remove-team-org.info"
|
||||
msgid "modals.remove-team-organization.info"
|
||||
msgstr ""
|
||||
"Les projets et fichiers demeureront disponibles aux membres de l'équipe, "
|
||||
"mais les paramètres de l'organisation ne s'appliqueront plus."
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:253
|
||||
msgid "modals.remove-team-org.text"
|
||||
msgid "modals.remove-team-organization.text"
|
||||
msgstr "Vraiment retirer l'équipe « %s » de l'organisation « %s »?"
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:252
|
||||
msgid "modals.remove-team-org.title"
|
||||
msgid "modals.remove-team-organization.title"
|
||||
msgstr "RETIRER L'ÉQUIPE DE L'ORGANISATION"
|
||||
|
||||
#: src/app/main/ui/workspace/nudge.cljs:52
|
||||
@ -4349,7 +4349,7 @@ msgid "nitrate.activation-success.active-until"
|
||||
msgstr "L'abonnement est actif jusqu'au %s."
|
||||
|
||||
#: src/app/main/ui/dashboard/subscription.cljs:195, src/app/main/ui/nitrate/nitrate_activation_success_modal.cljs:68
|
||||
msgid "nitrate.activation-success.create-org"
|
||||
msgid "nitrate.activation-success.create-organization"
|
||||
msgstr "Créer une organisation"
|
||||
|
||||
#: src/app/main/ui/nitrate/nitrate_activation_success_modal.cljs:63
|
||||
@ -4402,7 +4402,7 @@ msgstr ""
|
||||
"soit bien configuré."
|
||||
|
||||
#, unused
|
||||
msgid "nitrate.contact-sales.downgrade-org-deleted"
|
||||
msgid "nitrate.contact-sales.downgrade-organization-deleted"
|
||||
msgstr "Ton organisation sera supprimée."
|
||||
|
||||
#, unused
|
||||
@ -5707,7 +5707,7 @@ msgid "shortcuts.zoom-selected"
|
||||
msgstr "Zoomer à la sélection"
|
||||
|
||||
#: src/app/main/ui/dashboard/subscription.cljs:191
|
||||
msgid "subscription.banner.create-org-info"
|
||||
msgid "subscription.banner.create-organization-info"
|
||||
msgstr ""
|
||||
"Crée une organisation et vois exactement comment fonctionne Enterprise. "
|
||||
"Établis les règles, gère les équipes et profite d'un nouveau niveau de "
|
||||
|
||||
@ -352,7 +352,7 @@ msgid "dashboard.copy-suffix"
|
||||
msgstr "(עותק)"
|
||||
|
||||
#: src/app/main/ui/dashboard/sidebar.cljs:371
|
||||
msgid "dashboard.create-new-org"
|
||||
msgid "dashboard.create-new-organization"
|
||||
msgstr "יצירת ארגון חדש"
|
||||
|
||||
#: src/app/main/ui/dashboard/sidebar.cljs:439
|
||||
|
||||
@ -98,7 +98,7 @@ msgid "auth.new-password"
|
||||
msgstr "Inserisci una nuova password"
|
||||
|
||||
#: src/app/main/ui/auth/verify_token.cljs:53
|
||||
msgid "auth.notifications.org-invitation-accepted"
|
||||
msgid "auth.notifications.organization-invitation-accepted"
|
||||
msgstr "Ora fai parte di %s"
|
||||
|
||||
#: src/app/main/ui/auth/recovery.cljs:36
|
||||
@ -297,7 +297,7 @@ msgid "common.unpublish"
|
||||
msgstr "Rimuovi pubblicazione"
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:165
|
||||
msgid "dasboard.leave-org.toast"
|
||||
msgid "dashboard.leave-organization.toast"
|
||||
msgstr "Non fai più parte dell'organizzazione %s."
|
||||
|
||||
#: src/app/main/ui/dashboard/projects.cljs:90
|
||||
@ -365,25 +365,25 @@ msgid "dashboard.change-email"
|
||||
msgstr "Cambia indirizzo e-mail"
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.change-org-modal.accept"
|
||||
msgid "dashboard.change-organization-modal.accept"
|
||||
msgstr "SPOSTA TEAM"
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.change-org-modal.choose"
|
||||
msgid "dashboard.change-organization-modal.choose"
|
||||
msgstr "Sposta in:"
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.change-org-modal.select"
|
||||
msgid "dashboard.change-organization-modal.select"
|
||||
msgstr "Seleziona un'organizzazione"
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.change-org-modal.text"
|
||||
msgid "dashboard.change-organization-modal.text"
|
||||
msgstr ""
|
||||
"I progetti e i file rimarranno disponibili per i membri del team. Il team "
|
||||
"riceverà la configurazione dalla nuova organizzazione."
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:399, src/app/main/ui/dashboard/team_form.cljs:157
|
||||
msgid "dashboard.change-org-modal.title"
|
||||
msgid "dashboard.change-organization-modal.title"
|
||||
msgstr "Cambia organizzazione del team"
|
||||
|
||||
#: src/app/main/ui/dashboard/deleted.cljs:316
|
||||
@ -395,7 +395,7 @@ msgid "dashboard.copy-suffix"
|
||||
msgstr "(copia)"
|
||||
|
||||
#: src/app/main/ui/dashboard/sidebar.cljs:371
|
||||
msgid "dashboard.create-new-org"
|
||||
msgid "dashboard.create-new-organization"
|
||||
msgstr "Crea org"
|
||||
|
||||
#: src/app/main/ui/dashboard/sidebar.cljs:439
|
||||
@ -820,7 +820,7 @@ msgid "dashboard.invite-profile"
|
||||
msgstr "Invita nel team"
|
||||
|
||||
#: src/app/main/ui/dashboard/sidebar.cljs:671
|
||||
msgid "dashboard.leave-org"
|
||||
msgid "dashboard.leave-organization"
|
||||
msgstr "Lascia org"
|
||||
|
||||
#: src/app/main/ui/dashboard/sidebar.cljs:570, src/app/main/ui/dashboard/sidebar.cljs:577, src/app/main/ui/dashboard/sidebar.cljs:582, src/app/main/ui/dashboard/team.cljs:433
|
||||
@ -898,7 +898,7 @@ msgid "dashboard.no-matches-for"
|
||||
msgstr "Nessuna corrispondenza trovata per \"%s\""
|
||||
|
||||
#: src/app/main/ui/dashboard/team_form.cljs:156
|
||||
msgid "dashboard.no-org-allows-create-team.message"
|
||||
msgid "dashboard.no-organization-allows-create-team.message"
|
||||
msgstr "Non hai i permessi per aggiungere team a nessuna delle tue organizzazioni."
|
||||
|
||||
#: src/app/main/ui/dashboard/team_form.cljs:152
|
||||
@ -968,7 +968,7 @@ msgid "dashboard.order-invitations-by-status"
|
||||
msgstr "Ordina per stato"
|
||||
|
||||
#: src/app/main/data/dashboard.cljs:753
|
||||
msgid "dashboard.org-deleted"
|
||||
msgid "dashboard.organization-deleted"
|
||||
msgstr "L'organizzazione %s è stata eliminata."
|
||||
|
||||
#: src/app/main/ui/settings/password.cljs:96, src/app/main/ui/settings/password.cljs:109
|
||||
@ -1008,7 +1008,7 @@ msgid "dashboard.plugins.try-plugin"
|
||||
msgstr "Prova il plugin: "
|
||||
|
||||
#: src/app/main/ui/dashboard/sidebar.cljs:798
|
||||
msgid "dashboard.plus-create-new-org"
|
||||
msgid "dashboard.plus-create-new-organization"
|
||||
msgstr "+ Crea org"
|
||||
|
||||
#: src/app/main/data/dashboard.cljs:805
|
||||
@ -1095,36 +1095,36 @@ msgid "dashboard.section.organizations"
|
||||
msgstr "Organizzazioni"
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.select-org-modal.accept"
|
||||
msgid "dashboard.select-organization-modal.accept"
|
||||
msgstr "AGGIUNGI ALL'ORGANIZZAZIONE"
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.select-org-modal.choose"
|
||||
msgid "dashboard.select-organization-modal.choose"
|
||||
msgstr "Scegli un'organizzazione:"
|
||||
|
||||
#: src/app/main/ui/dashboard/team.cljs:975
|
||||
msgid "dashboard.select-org-modal.external-invitations-warning"
|
||||
msgid "dashboard.select-organization-modal.external-invitations-warning"
|
||||
msgstr ""
|
||||
"\"%s\" autorizza solo i membri dell'organizzazione ad unirsi al suo team. "
|
||||
"Qualsiasi invito in sospeso a persone al di fuori dell'organizzazione verrà "
|
||||
"revocato."
|
||||
|
||||
#: src/app/main/ui/dashboard/team.cljs:971
|
||||
msgid "dashboard.select-org-modal.external-invitations-will-be-canceled"
|
||||
msgid "dashboard.select-organization-modal.external-invitations-will-be-canceled"
|
||||
msgstr "Gli inviti in sospeso ad utenti esterni verranno annullati."
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.select-org-modal.permission-info"
|
||||
msgid "dashboard.select-organization-modal.permission-info"
|
||||
msgstr ""
|
||||
"Qui trovi tutte le organizzazioni dove sei autorizzato a creare o "
|
||||
"aggiungere team."
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.select-org-modal.select"
|
||||
msgid "dashboard.select-organization-modal.select"
|
||||
msgstr "Seleziona un'organizzazione"
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:342, src/app/main/ui/dashboard/team_form.cljs:155
|
||||
msgid "dashboard.select-org-modal.title"
|
||||
msgid "dashboard.select-organization-modal.title"
|
||||
msgstr "Aggiungi team ad una organizzazione"
|
||||
|
||||
#: src/app/main/ui/settings/options.cljs:66
|
||||
@ -1248,7 +1248,7 @@ msgid "dashboard.success-move-project"
|
||||
msgstr "Il tuo progetto è stato spostato con successo"
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.team-belong-org"
|
||||
msgid "dashboard.team-belong-organization"
|
||||
msgstr "Questo team ora appartiene a %s"
|
||||
|
||||
#: src/app/main/ui/dashboard/team.cljs:1602
|
||||
@ -1260,7 +1260,7 @@ msgid "dashboard.team-members"
|
||||
msgstr "Membri del team"
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.team-no-longer-belong-org"
|
||||
msgid "dashboard.team-no-longer-belong-organization"
|
||||
msgstr "Questo team non appartiene più all'organizzazione %s"
|
||||
|
||||
#: src/app/main/ui/dashboard/team.cljs:1609
|
||||
@ -1272,7 +1272,7 @@ msgid "dashboard.team-organization.add"
|
||||
msgstr "Aggiungi ad una organizzazione"
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:341, src/app/main/data/nitrate.cljs:398
|
||||
msgid "dashboard.team-organization.add.no-valid-orgs"
|
||||
msgid "dashboard.team-organization.add.no-valid-organizations"
|
||||
msgstr ""
|
||||
"Non hai i permessi per aggiungere questo team a nessuna delle tue "
|
||||
"organizzazioni."
|
||||
@ -1282,7 +1282,7 @@ msgid "dashboard.team-organization.change"
|
||||
msgstr "Cambia organizzazione del team"
|
||||
|
||||
#: src/app/main/ui/dashboard/team.cljs:905
|
||||
msgid "dashboard.team-organization.disabled-org-tooltip"
|
||||
msgid "dashboard.team-organization.disabled-organization-tooltip"
|
||||
msgstr ""
|
||||
"Solo le persone all'interno della tua organizzazione possono essere "
|
||||
"invitate."
|
||||
@ -1350,7 +1350,7 @@ msgid "dashboard.upgrade-plan.penpot-free"
|
||||
msgstr "Penpot Gratis"
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.user-no-longer-belong-org"
|
||||
msgid "dashboard.user-no-longer-belong-organization"
|
||||
msgstr "Non sei più un membro dell'organizzazione %s"
|
||||
|
||||
#: src/app/main/ui/settings/options.cljs:109
|
||||
@ -1731,17 +1731,17 @@ msgid "errors.only-creator-can-unlock"
|
||||
msgstr "Solo il creatore della versione può sbloccarla"
|
||||
|
||||
#, unused
|
||||
msgid "errors.org-leave.no-valid-teams"
|
||||
msgid "errors.organization-leave.no-valid-teams"
|
||||
msgstr ""
|
||||
"Si è verificato un problema durante l'abbandono dell'organizzazione. "
|
||||
"Riprova."
|
||||
|
||||
#, unused
|
||||
msgid "errors.org-leave.org-owner-cannot-leave"
|
||||
msgid "errors.organization-leave.organization-owner-cannot-leave"
|
||||
msgstr "Il proprietario dell'organizzazione non può lasciare l'organizzazione."
|
||||
|
||||
#: src/app/main/ui/auth/verify_token.cljs:97
|
||||
msgid "errors.org-not-found"
|
||||
msgid "errors.organization-not-found"
|
||||
msgstr "L'organizzazione non esiste"
|
||||
|
||||
#: src/app/main/ui/settings/password.cljs
|
||||
|
||||
@ -344,7 +344,7 @@ msgid "dashboard.copy-suffix"
|
||||
msgstr "(복사)"
|
||||
|
||||
#: src/app/main/ui/dashboard/sidebar.cljs:371
|
||||
msgid "dashboard.create-new-org"
|
||||
msgid "dashboard.create-new-organization"
|
||||
msgstr "새 조직 생성"
|
||||
|
||||
#: src/app/main/ui/dashboard/sidebar.cljs:439
|
||||
|
||||
@ -98,7 +98,7 @@ msgid "auth.new-password"
|
||||
msgstr "പുതിയൊരു പാസ്വേഡ് ചേർക്കുക"
|
||||
|
||||
#: src/app/main/ui/auth/verify_token.cljs:53
|
||||
msgid "auth.notifications.org-invitation-accepted"
|
||||
msgid "auth.notifications.organization-invitation-accepted"
|
||||
msgstr "നിങ്ങളിപ്പോൾ %s-ന്റെ ഭാഗമാണ്"
|
||||
|
||||
#: src/app/main/ui/auth/recovery.cljs:36
|
||||
|
||||
@ -363,7 +363,7 @@ msgid "dashboard.copy-suffix"
|
||||
msgstr "(kopie)"
|
||||
|
||||
#: src/app/main/ui/dashboard/sidebar.cljs:371
|
||||
msgid "dashboard.create-new-org"
|
||||
msgid "dashboard.create-new-organization"
|
||||
msgstr "Nieuwe org aanmaken"
|
||||
|
||||
#: src/app/main/ui/dashboard/sidebar.cljs:439
|
||||
|
||||
@ -364,7 +364,7 @@ msgid "dashboard.copy-suffix"
|
||||
msgstr "(копия)"
|
||||
|
||||
#: src/app/main/ui/dashboard/sidebar.cljs:371
|
||||
msgid "dashboard.create-new-org"
|
||||
msgid "dashboard.create-new-organization"
|
||||
msgstr "Создать новую организацию"
|
||||
|
||||
#: src/app/main/ui/dashboard/sidebar.cljs:439
|
||||
|
||||
@ -96,7 +96,7 @@ msgid "auth.new-password"
|
||||
msgstr "Skriv ett nytt lösenord"
|
||||
|
||||
#: src/app/main/ui/auth/verify_token.cljs:53
|
||||
msgid "auth.notifications.org-invitation-accepted"
|
||||
msgid "auth.notifications.organization-invitation-accepted"
|
||||
msgstr "Du är nu med i %s"
|
||||
|
||||
#: src/app/main/ui/auth/recovery.cljs:36
|
||||
@ -293,7 +293,7 @@ msgid "common.unpublish"
|
||||
msgstr "Avpublicera"
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:165
|
||||
msgid "dasboard.leave-org.toast"
|
||||
msgid "dashboard.leave-organization.toast"
|
||||
msgstr "Du är inte längre med i organisationen %s."
|
||||
|
||||
#: src/app/main/ui/dashboard/projects.cljs:90
|
||||
@ -363,25 +363,25 @@ msgid "dashboard.change-email"
|
||||
msgstr "Ändra e-post"
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.change-org-modal.accept"
|
||||
msgid "dashboard.change-organization-modal.accept"
|
||||
msgstr "FLYTTA TEAM"
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.change-org-modal.choose"
|
||||
msgid "dashboard.change-organization-modal.choose"
|
||||
msgstr "Flytta till:"
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.change-org-modal.select"
|
||||
msgid "dashboard.change-organization-modal.select"
|
||||
msgstr "Välj en organisation"
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.change-org-modal.text"
|
||||
msgid "dashboard.change-organization-modal.text"
|
||||
msgstr ""
|
||||
"Projekt och filer kommer att förbli tillgängliga för teammedlemmar. Teamet "
|
||||
"kommer att hämta konfigurationen från den nya organisationen."
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:399, src/app/main/ui/dashboard/team_form.cljs:157
|
||||
msgid "dashboard.change-org-modal.title"
|
||||
msgid "dashboard.change-organization-modal.title"
|
||||
msgstr "Ändra teamets organisation"
|
||||
|
||||
#: src/app/main/ui/dashboard/deleted.cljs:316
|
||||
@ -393,7 +393,7 @@ msgid "dashboard.copy-suffix"
|
||||
msgstr "(kopia)"
|
||||
|
||||
#: src/app/main/ui/dashboard/sidebar.cljs:371
|
||||
msgid "dashboard.create-new-org"
|
||||
msgid "dashboard.create-new-organization"
|
||||
msgstr "Skapa org"
|
||||
|
||||
#: src/app/main/ui/dashboard/sidebar.cljs:439
|
||||
@ -812,7 +812,7 @@ msgid "dashboard.invite-profile"
|
||||
msgstr "Bjud in personer"
|
||||
|
||||
#: src/app/main/ui/dashboard/sidebar.cljs:671
|
||||
msgid "dashboard.leave-org"
|
||||
msgid "dashboard.leave-organization"
|
||||
msgstr "Lämna org"
|
||||
|
||||
#: src/app/main/ui/dashboard/sidebar.cljs:570, src/app/main/ui/dashboard/sidebar.cljs:577, src/app/main/ui/dashboard/sidebar.cljs:582, src/app/main/ui/dashboard/team.cljs:433
|
||||
@ -888,7 +888,7 @@ msgid "dashboard.no-matches-for"
|
||||
msgstr "Inga träffar hittades för ”%s”"
|
||||
|
||||
#: src/app/main/ui/dashboard/team_form.cljs:156
|
||||
msgid "dashboard.no-org-allows-create-team.message"
|
||||
msgid "dashboard.no-organization-allows-create-team.message"
|
||||
msgstr "Du har inte behörighet att lägga till team i någon av dina organisationer."
|
||||
|
||||
#: src/app/main/ui/dashboard/team_form.cljs:152
|
||||
@ -958,7 +958,7 @@ msgid "dashboard.order-invitations-by-status"
|
||||
msgstr "Sortera efter status"
|
||||
|
||||
#: src/app/main/data/dashboard.cljs:753
|
||||
msgid "dashboard.org-deleted"
|
||||
msgid "dashboard.organization-deleted"
|
||||
msgstr "Organisationen %s har tagits bort."
|
||||
|
||||
#: src/app/main/ui/settings/password.cljs:96, src/app/main/ui/settings/password.cljs:109
|
||||
@ -998,7 +998,7 @@ msgid "dashboard.plugins.try-plugin"
|
||||
msgstr "Testa plugin: "
|
||||
|
||||
#: src/app/main/ui/dashboard/sidebar.cljs:798
|
||||
msgid "dashboard.plus-create-new-org"
|
||||
msgid "dashboard.plus-create-new-organization"
|
||||
msgstr "+ Skapa org"
|
||||
|
||||
#: src/app/main/data/dashboard.cljs:805
|
||||
@ -1083,36 +1083,36 @@ msgid "dashboard.section.organizations"
|
||||
msgstr "Organisationer"
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.select-org-modal.accept"
|
||||
msgid "dashboard.select-organization-modal.accept"
|
||||
msgstr "LÄGG TILL I ORGANISATION"
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.select-org-modal.choose"
|
||||
msgid "dashboard.select-organization-modal.choose"
|
||||
msgstr "Välj en organisation:"
|
||||
|
||||
#: src/app/main/ui/dashboard/team.cljs:975
|
||||
msgid "dashboard.select-org-modal.external-invitations-warning"
|
||||
msgid "dashboard.select-organization-modal.external-invitations-warning"
|
||||
msgstr ""
|
||||
"\"%s\" tillåter endast organisationsmedlemmar att gå med i dess team. Alla "
|
||||
"väntande inbjudningar till personer utanför organisationen kommer att "
|
||||
"återkallas."
|
||||
|
||||
#: src/app/main/ui/dashboard/team.cljs:971
|
||||
msgid "dashboard.select-org-modal.external-invitations-will-be-canceled"
|
||||
msgid "dashboard.select-organization-modal.external-invitations-will-be-canceled"
|
||||
msgstr "Väntande inbjudningar till externa användare kommer att avbrytas."
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.select-org-modal.permission-info"
|
||||
msgid "dashboard.select-organization-modal.permission-info"
|
||||
msgstr ""
|
||||
"Här hittar du alla dina organisationer där du har rätt att skapa eller "
|
||||
"lägga till team."
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.select-org-modal.select"
|
||||
msgid "dashboard.select-organization-modal.select"
|
||||
msgstr "Välj en organisation"
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:342, src/app/main/ui/dashboard/team_form.cljs:155
|
||||
msgid "dashboard.select-org-modal.title"
|
||||
msgid "dashboard.select-organization-modal.title"
|
||||
msgstr "Lägg till team i en organisation"
|
||||
|
||||
#: src/app/main/ui/settings/options.cljs:66
|
||||
@ -1236,7 +1236,7 @@ msgid "dashboard.success-move-project"
|
||||
msgstr "Ditt projekt har flyttats"
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.team-belong-org"
|
||||
msgid "dashboard.team-belong-organization"
|
||||
msgstr "Detta team tillhör nu %s"
|
||||
|
||||
#: src/app/main/ui/dashboard/team.cljs:1602
|
||||
@ -1248,7 +1248,7 @@ msgid "dashboard.team-members"
|
||||
msgstr "Teammedlemmar"
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.team-no-longer-belong-org"
|
||||
msgid "dashboard.team-no-longer-belong-organization"
|
||||
msgstr "Detta team tillhör inte längre organisation %s"
|
||||
|
||||
#: src/app/main/ui/dashboard/team.cljs:1609
|
||||
@ -1260,7 +1260,7 @@ msgid "dashboard.team-organization.add"
|
||||
msgstr "Lägg till i en organisation"
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:341, src/app/main/data/nitrate.cljs:398
|
||||
msgid "dashboard.team-organization.add.no-valid-orgs"
|
||||
msgid "dashboard.team-organization.add.no-valid-organizations"
|
||||
msgstr ""
|
||||
"Du har inte behörighet att lägga till det här teamet i någon av dina "
|
||||
"organisationer."
|
||||
@ -1270,7 +1270,7 @@ msgid "dashboard.team-organization.change"
|
||||
msgstr "Ändra teamorganisation"
|
||||
|
||||
#: src/app/main/ui/dashboard/team.cljs:905
|
||||
msgid "dashboard.team-organization.disabled-org-tooltip"
|
||||
msgid "dashboard.team-organization.disabled-organization-tooltip"
|
||||
msgstr "Endast personer inom din organisation kan bjudas in."
|
||||
|
||||
#: src/app/main/ui/dashboard/team.cljs:1639
|
||||
@ -1336,7 +1336,7 @@ msgid "dashboard.upgrade-plan.penpot-free"
|
||||
msgstr "Gratis Penpot"
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.user-no-longer-belong-org"
|
||||
msgid "dashboard.user-no-longer-belong-organization"
|
||||
msgstr "Du är inte längre medlem i organisation %s"
|
||||
|
||||
#: src/app/main/ui/settings/options.cljs:109
|
||||
@ -1712,15 +1712,15 @@ msgid "errors.only-creator-can-unlock"
|
||||
msgstr "Endast skaparen av versionen kan låsa upp den"
|
||||
|
||||
#, unused
|
||||
msgid "errors.org-leave.no-valid-teams"
|
||||
msgid "errors.organization-leave.no-valid-teams"
|
||||
msgstr "Det uppstod ett fel när du skulle lämna organisationen. Försök igen."
|
||||
|
||||
#, unused
|
||||
msgid "errors.org-leave.org-owner-cannot-leave"
|
||||
msgid "errors.organization-leave.organization-owner-cannot-leave"
|
||||
msgstr "Organisationsägaren kan inte lämna organisationen."
|
||||
|
||||
#: src/app/main/ui/auth/verify_token.cljs:97
|
||||
msgid "errors.org-not-found"
|
||||
msgid "errors.organization-not-found"
|
||||
msgstr "Den organisationen finns inte"
|
||||
|
||||
#: src/app/main/ui/settings/password.cljs
|
||||
@ -3731,17 +3731,17 @@ msgid "modals.add-shared-confirm.message"
|
||||
msgstr "Lägg till \"%s\" som delat bibliotek"
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:199
|
||||
msgid "modals.before-leave-org.message"
|
||||
msgid "modals.before-leave-organization.message"
|
||||
msgstr ""
|
||||
"Du är den enda medlemmen i vissa av teamen. Dessa team kommer att raderas "
|
||||
"tillsammans med deras projekt och filer."
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:198, src/app/main/ui/dashboard/change_owner.cljs:151
|
||||
msgid "modals.before-leave-org.title"
|
||||
msgid "modals.before-leave-organization.title"
|
||||
msgstr "INNAN DU LÄMNAR ORGANISATIONEN"
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:202
|
||||
msgid "modals.before-leave-org.warning"
|
||||
msgid "modals.before-leave-organization.warning"
|
||||
msgstr "Alla team där du är den enda medlemmen kommer att raderas."
|
||||
|
||||
#: src/app/main/ui/workspace/nudge.cljs:59
|
||||
@ -3801,22 +3801,22 @@ msgstr "Genom att ta bort ditt konto förlorar du alla dina projekt och arkiv."
|
||||
|
||||
#: src/app/main/ui/settings/delete_account.cljs
|
||||
#, unused
|
||||
msgid "modals.delete-account.info.with-orgs"
|
||||
msgid "modals.delete-account.info.with-organizations"
|
||||
msgstr ""
|
||||
"Denna åtgärd kan inte ångras. Ditt konto, din prenumeration och dina "
|
||||
"organisationer kommer att raderas omedelbart. Du kommer också att förlora "
|
||||
"åtkomsten till dina nuvarande projekt och filer."
|
||||
|
||||
#: src/app/main/ui/settings/delete_account.cljs:83
|
||||
msgid "modals.delete-account.owned-orgs.list-title"
|
||||
msgid "modals.delete-account.owned-organizations.list-title"
|
||||
msgstr "Organisationer som kommer att raderas"
|
||||
|
||||
#: src/app/main/ui/settings/delete_account.cljs:98
|
||||
msgid "modals.delete-account.owned-orgs.members-count"
|
||||
msgid "modals.delete-account.owned-organizations.members-count"
|
||||
msgstr "%s medlemmar"
|
||||
|
||||
#: src/app/main/ui/settings/delete_account.cljs:96
|
||||
msgid "modals.delete-account.owned-orgs.teams-count"
|
||||
msgid "modals.delete-account.owned-organizations.teams-count"
|
||||
msgstr "%s team"
|
||||
|
||||
#: src/app/main/ui/settings/delete_account.cljs:64
|
||||
@ -3900,7 +3900,7 @@ msgid "modals.delete-font.title"
|
||||
msgstr "Tar bort typsnitt"
|
||||
|
||||
#: src/app/main/data/team.cljs:129
|
||||
msgid "modals.delete-org-team-confirm.message"
|
||||
msgid "modals.delete-organization-team-confirm.message"
|
||||
msgstr ""
|
||||
"Är du säker på att du vill ta bort det här teamet som är en del av "
|
||||
"organisation %s?"
|
||||
@ -4048,7 +4048,7 @@ msgid "modals.invite-restricted-members.all-blocked-title"
|
||||
msgstr "Inga inbjudningar skickades"
|
||||
|
||||
#: src/app/main/data/team.cljs:162
|
||||
msgid "modals.invite-restricted-members.all-org-members-in-team"
|
||||
msgid "modals.invite-restricted-members.all-organization-members-in-team"
|
||||
msgstr ""
|
||||
"Endast medlemmar i organisationen \"%s\" kan bjudas in till det här teamet, "
|
||||
"och de är alla redan teammedlemmar. Om du behöver mer information, kontakta "
|
||||
@ -4135,35 +4135,35 @@ msgid "modals.leave-confirm.title"
|
||||
msgstr "Lämnar teamet"
|
||||
|
||||
#: src/app/main/ui/dashboard/change_owner.cljs:158
|
||||
msgid "modals.leave-org-and-reassign.hint"
|
||||
msgid "modals.leave-organization-and-reassign.hint"
|
||||
msgstr ""
|
||||
"Du är ägare till teamen i vissa organisationer. Vänligen befordra en annan "
|
||||
"medlem till att bli ägare."
|
||||
|
||||
#: src/app/main/ui/dashboard/change_owner.cljs:161
|
||||
msgid "modals.leave-org-and-reassign.hint-delete"
|
||||
msgid "modals.leave-organization-and-reassign.hint-delete"
|
||||
msgstr ""
|
||||
"Du är den enda medlemmen i vissa av teamen. Dessa team kommer att raderas "
|
||||
"tillsammans med deras projekt och filer."
|
||||
|
||||
#: src/app/main/ui/dashboard/change_owner.cljs:163
|
||||
msgid "modals.leave-org-and-reassign.hint-promote"
|
||||
msgid "modals.leave-organization-and-reassign.hint-promote"
|
||||
msgstr ""
|
||||
"Du är också ägare till vissa organisationers team. Vänligen befordra en "
|
||||
"annan medlem till att bli ägare."
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:200, src/app/main/data/nitrate.cljs:209
|
||||
msgid "modals.leave-org-confirm.accept"
|
||||
msgid "modals.leave-organization-confirm.accept"
|
||||
msgstr "Lämna organisation"
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:208
|
||||
msgid "modals.leave-org-confirm.message"
|
||||
msgid "modals.leave-organization-confirm.message"
|
||||
msgstr ""
|
||||
"Du kommer permanent att förlora åtkomsten till alla team, projekt och filer "
|
||||
"i den."
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:207
|
||||
msgid "modals.leave-org-confirm.title"
|
||||
msgid "modals.leave-organization-confirm.title"
|
||||
msgstr "Lämnar du organisation %s?"
|
||||
|
||||
#: src/app/main/ui/delete_shared.cljs:54
|
||||
@ -4239,21 +4239,21 @@ msgid "modals.remove-shared-confirm.message"
|
||||
msgstr "Ta bort \"%s\" som delat bibliotek"
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:256
|
||||
msgid "modals.remove-team-org.accept"
|
||||
msgid "modals.remove-team-organization.accept"
|
||||
msgstr "Ta bort från organisationen"
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:254
|
||||
msgid "modals.remove-team-org.info"
|
||||
msgid "modals.remove-team-organization.info"
|
||||
msgstr ""
|
||||
"Projekt och filer kommer att förbli tillgängliga för teammedlemmar, men "
|
||||
"organisationens inställningar kommer inte längre att gälla."
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:253
|
||||
msgid "modals.remove-team-org.text"
|
||||
msgid "modals.remove-team-organization.text"
|
||||
msgstr "Är du säker på att du vill ta bort teamet '%s' från organisationen '%s'?"
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:252
|
||||
msgid "modals.remove-team-org.title"
|
||||
msgid "modals.remove-team-organization.title"
|
||||
msgstr "TA BORT TEAMET FRÅN ORGANISATIONEN"
|
||||
|
||||
#: src/app/main/ui/workspace/nudge.cljs:52
|
||||
@ -4321,7 +4321,7 @@ msgid "nitrate.activation-success.active-until"
|
||||
msgstr "Ditt abonnemang är aktivt till %s."
|
||||
|
||||
#: src/app/main/ui/dashboard/subscription.cljs:195, src/app/main/ui/nitrate/nitrate_activation_success_modal.cljs:68
|
||||
msgid "nitrate.activation-success.create-org"
|
||||
msgid "nitrate.activation-success.create-organization"
|
||||
msgstr "Skapa organisation"
|
||||
|
||||
#: src/app/main/ui/nitrate/nitrate_activation_success_modal.cljs:63
|
||||
@ -4374,7 +4374,7 @@ msgstr ""
|
||||
"korrekt konfigurerat."
|
||||
|
||||
#, unused
|
||||
msgid "nitrate.contact-sales.downgrade-org-deleted"
|
||||
msgid "nitrate.contact-sales.downgrade-organization-deleted"
|
||||
msgstr "Din organisation kommer att raderas."
|
||||
|
||||
#, unused
|
||||
@ -5671,7 +5671,7 @@ msgid "shortcuts.zoom-selected"
|
||||
msgstr "Zooma in markerad"
|
||||
|
||||
#: src/app/main/ui/dashboard/subscription.cljs:191
|
||||
msgid "subscription.banner.create-org-info"
|
||||
msgid "subscription.banner.create-organization-info"
|
||||
msgstr ""
|
||||
"Skapa en organisation och se exakt hur Enterprise fungerar. Sätt regler, "
|
||||
"hantera dina team och upptäck en ny nivå av kontroll över ditt "
|
||||
|
||||
@ -98,7 +98,7 @@ msgid "auth.new-password"
|
||||
msgstr "Yeni bir parola gir"
|
||||
|
||||
#: src/app/main/ui/auth/verify_token.cljs:53
|
||||
msgid "auth.notifications.org-invitation-accepted"
|
||||
msgid "auth.notifications.organization-invitation-accepted"
|
||||
msgstr "Artık %s organizasyonunun bir parçasısınız"
|
||||
|
||||
#: src/app/main/ui/auth/recovery.cljs:36
|
||||
@ -294,7 +294,7 @@ msgid "common.unpublish"
|
||||
msgstr "Yayından kaldır"
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:165
|
||||
msgid "dasboard.leave-org.toast"
|
||||
msgid "dashboard.leave-organization.toast"
|
||||
msgstr "Artık %s organizasyonunun bir parçası değilsiniz."
|
||||
|
||||
#: src/app/main/ui/dashboard/projects.cljs:90
|
||||
@ -364,25 +364,25 @@ msgid "dashboard.change-email"
|
||||
msgstr "E-posta adresini değiştir"
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.change-org-modal.accept"
|
||||
msgid "dashboard.change-organization-modal.accept"
|
||||
msgstr "TAKIMI TAŞI"
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.change-org-modal.choose"
|
||||
msgid "dashboard.change-organization-modal.choose"
|
||||
msgstr "Şuraya taşı:"
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.change-org-modal.select"
|
||||
msgid "dashboard.change-organization-modal.select"
|
||||
msgstr "Organizasyon seç"
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.change-org-modal.text"
|
||||
msgid "dashboard.change-organization-modal.text"
|
||||
msgstr ""
|
||||
"Projeler ve dosyalar takım üyeleri tarafından erişilebilir olmaya devam "
|
||||
"edecek. Takım, yeni organizasyondan yapılandırmayı alacak."
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:399, src/app/main/ui/dashboard/team_form.cljs:157
|
||||
msgid "dashboard.change-org-modal.title"
|
||||
msgid "dashboard.change-organization-modal.title"
|
||||
msgstr "Takımın organizasyonunu değiştir"
|
||||
|
||||
#: src/app/main/ui/dashboard/deleted.cljs:316
|
||||
@ -394,7 +394,7 @@ msgid "dashboard.copy-suffix"
|
||||
msgstr "(kopya)"
|
||||
|
||||
#: src/app/main/ui/dashboard/sidebar.cljs:371
|
||||
msgid "dashboard.create-new-org"
|
||||
msgid "dashboard.create-new-organization"
|
||||
msgstr "Yeni organizasyon oluştur"
|
||||
|
||||
#: src/app/main/ui/dashboard/sidebar.cljs:439
|
||||
@ -816,7 +816,7 @@ msgid "dashboard.invite-profile"
|
||||
msgstr "İnsanları davet et"
|
||||
|
||||
#: src/app/main/ui/dashboard/sidebar.cljs:671
|
||||
msgid "dashboard.leave-org"
|
||||
msgid "dashboard.leave-organization"
|
||||
msgstr "Organizasyondan ayrıl"
|
||||
|
||||
#: src/app/main/ui/dashboard/sidebar.cljs:570, src/app/main/ui/dashboard/sidebar.cljs:577, src/app/main/ui/dashboard/sidebar.cljs:582, src/app/main/ui/dashboard/team.cljs:433
|
||||
@ -894,7 +894,7 @@ msgid "dashboard.no-matches-for"
|
||||
msgstr "\"%s\" için sonuç bulunamadı"
|
||||
|
||||
#: src/app/main/ui/dashboard/team_form.cljs:156
|
||||
msgid "dashboard.no-org-allows-create-team.message"
|
||||
msgid "dashboard.no-organization-allows-create-team.message"
|
||||
msgstr "Hiçbir organizasyonunuza takım ekleme izniniz yok."
|
||||
|
||||
#: src/app/main/ui/dashboard/team_form.cljs:152
|
||||
@ -964,7 +964,7 @@ msgid "dashboard.order-invitations-by-status"
|
||||
msgstr "Duruma göre sırala"
|
||||
|
||||
#: src/app/main/data/dashboard.cljs:753
|
||||
msgid "dashboard.org-deleted"
|
||||
msgid "dashboard.organization-deleted"
|
||||
msgstr "%s organizasyonu silindi."
|
||||
|
||||
#: src/app/main/ui/settings/password.cljs:96, src/app/main/ui/settings/password.cljs:109
|
||||
@ -1004,7 +1004,7 @@ msgid "dashboard.plugins.try-plugin"
|
||||
msgstr "Eklentiyi deneyin: "
|
||||
|
||||
#: src/app/main/ui/dashboard/sidebar.cljs:798
|
||||
msgid "dashboard.plus-create-new-org"
|
||||
msgid "dashboard.plus-create-new-organization"
|
||||
msgstr "+ Organizasyon oluştur"
|
||||
|
||||
#: src/app/main/data/dashboard.cljs:805
|
||||
@ -1091,36 +1091,36 @@ msgid "dashboard.section.organizations"
|
||||
msgstr "Organizasyonlar"
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.select-org-modal.accept"
|
||||
msgid "dashboard.select-organization-modal.accept"
|
||||
msgstr "ORGANİZASYONA EKLE"
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.select-org-modal.choose"
|
||||
msgid "dashboard.select-organization-modal.choose"
|
||||
msgstr "Organizasyon seç:"
|
||||
|
||||
#: src/app/main/ui/dashboard/team.cljs:975
|
||||
msgid "dashboard.select-org-modal.external-invitations-warning"
|
||||
msgid "dashboard.select-organization-modal.external-invitations-warning"
|
||||
msgstr ""
|
||||
"\"%s\", yalnızca organizasyon üyelerinin takımlarına katılmasına izin "
|
||||
"veriyor. Organizasyon dışındaki kişilere gönderilmiş bekleyen davetler "
|
||||
"iptal edilecektir."
|
||||
|
||||
#: src/app/main/ui/dashboard/team.cljs:971
|
||||
msgid "dashboard.select-org-modal.external-invitations-will-be-canceled"
|
||||
msgid "dashboard.select-organization-modal.external-invitations-will-be-canceled"
|
||||
msgstr "Dış kullanıcılara gönderilen bekleyen davetler iptal edilecektir."
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.select-org-modal.permission-info"
|
||||
msgid "dashboard.select-organization-modal.permission-info"
|
||||
msgstr ""
|
||||
"Burada, takım oluşturmanıza veya eklemenize izin verilen tüm "
|
||||
"organizasyonlarınızı bulabilirsiniz."
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.select-org-modal.select"
|
||||
msgid "dashboard.select-organization-modal.select"
|
||||
msgstr "Organizasyon seç"
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:342, src/app/main/ui/dashboard/team_form.cljs:155
|
||||
msgid "dashboard.select-org-modal.title"
|
||||
msgid "dashboard.select-organization-modal.title"
|
||||
msgstr "Takımı bir organizasyona ekle"
|
||||
|
||||
#: src/app/main/ui/settings/options.cljs:66
|
||||
@ -1244,7 +1244,7 @@ msgid "dashboard.success-move-project"
|
||||
msgstr "Projen başarıyla taşındı"
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.team-belong-org"
|
||||
msgid "dashboard.team-belong-organization"
|
||||
msgstr "Bu takım artık %s organizasyonuna ait"
|
||||
|
||||
#: src/app/main/ui/dashboard/team.cljs:1602
|
||||
@ -1256,7 +1256,7 @@ msgid "dashboard.team-members"
|
||||
msgstr "Takım üyeleri"
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.team-no-longer-belong-org"
|
||||
msgid "dashboard.team-no-longer-belong-organization"
|
||||
msgstr "Bu takım artık %s organizasyonuna ait değil"
|
||||
|
||||
#: src/app/main/ui/dashboard/team.cljs:1609
|
||||
@ -1268,7 +1268,7 @@ msgid "dashboard.team-organization.add"
|
||||
msgstr "Organizasyona ekle"
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:341, src/app/main/data/nitrate.cljs:398
|
||||
msgid "dashboard.team-organization.add.no-valid-orgs"
|
||||
msgid "dashboard.team-organization.add.no-valid-organizations"
|
||||
msgstr "Bu takımı hiçbir organizasyonunuza ekleme izniniz yok."
|
||||
|
||||
#: src/app/main/ui/dashboard/team.cljs:1632
|
||||
@ -1276,7 +1276,7 @@ msgid "dashboard.team-organization.change"
|
||||
msgstr "Takım organizasyonunu değiştir"
|
||||
|
||||
#: src/app/main/ui/dashboard/team.cljs:905
|
||||
msgid "dashboard.team-organization.disabled-org-tooltip"
|
||||
msgid "dashboard.team-organization.disabled-organization-tooltip"
|
||||
msgstr "Yalnızca organizasyonunuzdaki kişiler davet edilebilir."
|
||||
|
||||
#: src/app/main/ui/dashboard/team.cljs:1639
|
||||
@ -1342,7 +1342,7 @@ msgid "dashboard.upgrade-plan.penpot-free"
|
||||
msgstr "Penpot Ücretsiz"
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.user-no-longer-belong-org"
|
||||
msgid "dashboard.user-no-longer-belong-organization"
|
||||
msgstr "Artık %s organizasyonunun üyesi değilsiniz"
|
||||
|
||||
#: src/app/main/ui/settings/options.cljs:109
|
||||
@ -1719,15 +1719,15 @@ msgid "errors.only-creator-can-unlock"
|
||||
msgstr "Yalnızca sürüm oluşturucu onun kilidini açabilir"
|
||||
|
||||
#, unused
|
||||
msgid "errors.org-leave.no-valid-teams"
|
||||
msgid "errors.organization-leave.no-valid-teams"
|
||||
msgstr "Organizasyondan ayrılma sırasında bir hata oluştu. Lütfen tekrar deneyin."
|
||||
|
||||
#, unused
|
||||
msgid "errors.org-leave.org-owner-cannot-leave"
|
||||
msgid "errors.organization-leave.organization-owner-cannot-leave"
|
||||
msgstr "Organizasyon sahibi organizasyondan ayrılamaz."
|
||||
|
||||
#: src/app/main/ui/auth/verify_token.cljs:97
|
||||
msgid "errors.org-not-found"
|
||||
msgid "errors.organization-not-found"
|
||||
msgstr "Bu organizasyon yok"
|
||||
|
||||
#: src/app/main/ui/settings/password.cljs
|
||||
@ -3737,17 +3737,17 @@ msgid "modals.add-shared-confirm.message"
|
||||
msgstr "Paylaşılmış Kütüphane olarak “%s” Ekle"
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:199
|
||||
msgid "modals.before-leave-org.message"
|
||||
msgid "modals.before-leave-organization.message"
|
||||
msgstr ""
|
||||
"Bazı takımların tek üyesi sizsiniz. Bu takımlar, projeleriyle ve "
|
||||
"dosyalarıyla birlikte silinecek."
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:198, src/app/main/ui/dashboard/change_owner.cljs:151
|
||||
msgid "modals.before-leave-org.title"
|
||||
msgid "modals.before-leave-organization.title"
|
||||
msgstr "ORGANİZASYONDAN AYRILMADAN ÖNCE"
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:202
|
||||
msgid "modals.before-leave-org.warning"
|
||||
msgid "modals.before-leave-organization.warning"
|
||||
msgstr "Tek üyesi olduğunuz tüm takımlar silinecektir."
|
||||
|
||||
#: src/app/main/ui/workspace/nudge.cljs:59
|
||||
@ -3807,22 +3807,22 @@ msgstr "Hesabını silerek tüm projelerini ve arşivlerini kaybedeceksin."
|
||||
|
||||
#: src/app/main/ui/settings/delete_account.cljs
|
||||
#, unused
|
||||
msgid "modals.delete-account.info.with-orgs"
|
||||
msgid "modals.delete-account.info.with-organizations"
|
||||
msgstr ""
|
||||
"Bu işlem geri alınamaz. Hesabınız, aboneliğiniz ve organizasyonlarınız "
|
||||
"hemen silinecektir. Ayrıca var olan projelerinize ve dosyalarınıza "
|
||||
"erişiminizi de kaybedeceksiniz."
|
||||
|
||||
#: src/app/main/ui/settings/delete_account.cljs:83
|
||||
msgid "modals.delete-account.owned-orgs.list-title"
|
||||
msgid "modals.delete-account.owned-organizations.list-title"
|
||||
msgstr "Silinecek organizasyonlar"
|
||||
|
||||
#: src/app/main/ui/settings/delete_account.cljs:98
|
||||
msgid "modals.delete-account.owned-orgs.members-count"
|
||||
msgid "modals.delete-account.owned-organizations.members-count"
|
||||
msgstr "%s üye"
|
||||
|
||||
#: src/app/main/ui/settings/delete_account.cljs:96
|
||||
msgid "modals.delete-account.owned-orgs.teams-count"
|
||||
msgid "modals.delete-account.owned-organizations.teams-count"
|
||||
msgstr "%s takım"
|
||||
|
||||
#: src/app/main/ui/settings/delete_account.cljs:64
|
||||
@ -3906,7 +3906,7 @@ msgid "modals.delete-font.title"
|
||||
msgstr "Yazı tipini sil"
|
||||
|
||||
#: src/app/main/data/team.cljs:129
|
||||
msgid "modals.delete-org-team-confirm.message"
|
||||
msgid "modals.delete-organization-team-confirm.message"
|
||||
msgstr ""
|
||||
"%s organizasyonunun parçası olan bu takımı silmek istediğinizden emin "
|
||||
"misiniz?"
|
||||
@ -4054,7 +4054,7 @@ msgid "modals.invite-restricted-members.all-blocked-title"
|
||||
msgstr "Davet gönderilmedi"
|
||||
|
||||
#: src/app/main/data/team.cljs:162
|
||||
msgid "modals.invite-restricted-members.all-org-members-in-team"
|
||||
msgid "modals.invite-restricted-members.all-organization-members-in-team"
|
||||
msgstr ""
|
||||
"Bu takıma yalnızca \"%s\" organizasyonunun üyeleri davet edilebilir ve bu "
|
||||
"kişiler zaten takımın üyeleridir. Daha fazla bilgiye ihtiyacınız varsa, "
|
||||
@ -4141,35 +4141,35 @@ msgid "modals.leave-confirm.title"
|
||||
msgstr "Takımdan ayrıl"
|
||||
|
||||
#: src/app/main/ui/dashboard/change_owner.cljs:158
|
||||
msgid "modals.leave-org-and-reassign.hint"
|
||||
msgid "modals.leave-organization-and-reassign.hint"
|
||||
msgstr ""
|
||||
"Bazı organizasyon takımlarının sahibi sizsiniz. Lütfen başka bir üyeyi "
|
||||
"sahibi olarak atayın."
|
||||
|
||||
#: src/app/main/ui/dashboard/change_owner.cljs:161
|
||||
msgid "modals.leave-org-and-reassign.hint-delete"
|
||||
msgid "modals.leave-organization-and-reassign.hint-delete"
|
||||
msgstr ""
|
||||
"Bazı takımların tek üyesi sizsiniz. Bu takımlar, projeleriyle ve "
|
||||
"dosyalarıyla birlikte silinecek."
|
||||
|
||||
#: src/app/main/ui/dashboard/change_owner.cljs:163
|
||||
msgid "modals.leave-org-and-reassign.hint-promote"
|
||||
msgid "modals.leave-organization-and-reassign.hint-promote"
|
||||
msgstr ""
|
||||
"Ayrıca, bazı organizasyon takımlarının sahibi sizsiniz. Lütfen başka bir "
|
||||
"üyeyi sahibi olarak atayın."
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:200, src/app/main/data/nitrate.cljs:209
|
||||
msgid "modals.leave-org-confirm.accept"
|
||||
msgid "modals.leave-organization-confirm.accept"
|
||||
msgstr "Organizasyondan ayrıl"
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:208
|
||||
msgid "modals.leave-org-confirm.message"
|
||||
msgid "modals.leave-organization-confirm.message"
|
||||
msgstr ""
|
||||
"İçindeki tüm takımlara, projelere ve dosyalara erişiminizi kalıcı olarak "
|
||||
"kaybedeceksiniz."
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:207
|
||||
msgid "modals.leave-org-confirm.title"
|
||||
msgid "modals.leave-organization-confirm.title"
|
||||
msgstr "%s organizasyonundan ayrılıyor musunuz?"
|
||||
|
||||
#: src/app/main/ui/delete_shared.cljs:54
|
||||
@ -4244,21 +4244,21 @@ msgid "modals.remove-shared-confirm.message"
|
||||
msgstr "“%s” Paylaşılan Kütüphanesini Kaldır"
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:256
|
||||
msgid "modals.remove-team-org.accept"
|
||||
msgid "modals.remove-team-organization.accept"
|
||||
msgstr "Organizasyondan kaldır"
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:254
|
||||
msgid "modals.remove-team-org.info"
|
||||
msgid "modals.remove-team-organization.info"
|
||||
msgstr ""
|
||||
"Projeler ve dosyalar takım üyeleri tarafından kullanılabilir olmaya devam "
|
||||
"edecek, ancak organizasyonun ayarları artık geçerli olmayacaktır."
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:253
|
||||
msgid "modals.remove-team-org.text"
|
||||
msgid "modals.remove-team-organization.text"
|
||||
msgstr "'%s' takımını '%s' organizasyonundan kaldırmak istediğinizden emin misiniz?"
|
||||
|
||||
#: src/app/main/data/nitrate.cljs:252
|
||||
msgid "modals.remove-team-org.title"
|
||||
msgid "modals.remove-team-organization.title"
|
||||
msgstr "TAKIMI ORGANİZASYONDAN KALDIR"
|
||||
|
||||
#: src/app/main/ui/workspace/nudge.cljs:52
|
||||
@ -4326,7 +4326,7 @@ msgid "nitrate.activation-success.active-until"
|
||||
msgstr "Planınız %s tarihine kadar etkindir."
|
||||
|
||||
#: src/app/main/ui/dashboard/subscription.cljs:195, src/app/main/ui/nitrate/nitrate_activation_success_modal.cljs:68
|
||||
msgid "nitrate.activation-success.create-org"
|
||||
msgid "nitrate.activation-success.create-organization"
|
||||
msgstr "Organizasyon oluştur"
|
||||
|
||||
#: src/app/main/ui/nitrate/nitrate_activation_success_modal.cljs:63
|
||||
@ -4379,7 +4379,7 @@ msgstr ""
|
||||
"ayarlandığından emin olacağız."
|
||||
|
||||
#, unused
|
||||
msgid "nitrate.contact-sales.downgrade-org-deleted"
|
||||
msgid "nitrate.contact-sales.downgrade-organization-deleted"
|
||||
msgstr "Organizasyonunuz silinecek."
|
||||
|
||||
#, unused
|
||||
@ -5673,7 +5673,7 @@ msgid "shortcuts.zoom-selected"
|
||||
msgstr "Seçilene yakınlaştır"
|
||||
|
||||
#: src/app/main/ui/dashboard/subscription.cljs:191
|
||||
msgid "subscription.banner.create-org-info"
|
||||
msgid "subscription.banner.create-organization-info"
|
||||
msgstr ""
|
||||
"Bir organizasyon oluşturun ve Kurumsal planın tam olarak nasıl çalıştığını "
|
||||
"görün. Kurallar belirleyin, takımlarınızı yönetin ve tasarım iş akışınız "
|
||||
|
||||
@ -368,7 +368,7 @@ msgid "dashboard.copy-suffix"
|
||||
msgstr "(копія)"
|
||||
|
||||
#: src/app/main/ui/dashboard/sidebar.cljs:371
|
||||
msgid "dashboard.create-new-org"
|
||||
msgid "dashboard.create-new-organization"
|
||||
msgstr "Створити організацію"
|
||||
|
||||
#: src/app/main/ui/dashboard/sidebar.cljs:439
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user