diff --git a/frontend/resources/locales.json b/frontend/resources/locales.json index ce9d50b81..af9bad780 100644 --- a/frontend/resources/locales.json +++ b/frontend/resources/locales.json @@ -2670,6 +2670,30 @@ "es" : "Texto (T)" } }, + "workspace.updates.there-are-updates" : { + "translations" : { + "en" : "There are updates in shared libraries", + "fr" : "", + "ru" : "", + "es" : "Hay actualizaciones en librerías compartidas" + } + }, + "workspace.updates.update" : { + "translations" : { + "en" : "Update", + "fr" : "", + "ru" : "", + "es" : "Actualizar" + } + }, + "workspace.updates.dismiss" : { + "translations" : { + "en" : "Dismiss", + "fr" : "", + "ru" : "", + "es" : "Ignorar" + } + }, "workspace.viewport.click-to-close-path" : { "used-in" : [ "src/app/main/ui/workspace/drawarea.cljs:59" ], "translations" : { diff --git a/frontend/resources/styles/common/framework.scss b/frontend/resources/styles/common/framework.scss index f20199090..9a725c916 100644 --- a/frontend/resources/styles/common/framework.scss +++ b/frontend/resources/styles/common/framework.scss @@ -1014,20 +1014,67 @@ input[type=range]:focus::-ms-fill-upper { // Messages -// Banner top - .banner { - top: 0; - left: 0px; - width: 100%; - height: 40px; - z-index: 13; + position: relative; - display: flex; - justify-content: center; - align-items: center; + &.error { + background-color: $color-danger; + } - .btn-close { + &.success { + background-color: $color-success; + } + + &.warning { + background-color: $color-warning; + } + + &.info { + background-color: $color-info; + } + + &.hide { + @include animation(0, .6s, fadeOutUp); + } + + & .icon { + display: flex; + + svg { + fill: $color-white; + height: 20px; + width: 20px; + } + } + + & .content { + &.bottom-actions { + flex-direction: column; + + & .actions { + margin-top: $medium; + display: flex; + justify-content: flex-start; + } + } + + &.inline-actions { + flex-direction: row; + align-items: center; + justify-content: space-between; + + & .actions { + display: flex; + justify-content: flex-start; + + .btn-secondary { + margin-left: $medium; + } + } + } + } + + & .btn-close { position: absolute; right: 0px; top: 0px; @@ -1051,164 +1098,223 @@ input[type=range]:focus::-ms-fill-upper { opacity: .8; } } +} - .content { - align-items: center; - color: $color-white; +.banner.fixed { + position: fixed; + top: 0; + left: 0px; + width: 100%; + height: 40px; + z-index: 13; + + display: flex; + justify-content: center; + align-items: center; + + & .wrapper { display: flex; justify-content: center; + align-items: center; max-width: 60%; - .icon { - display: flex; + & .icon { margin-right: $medium; - svg { - fill: $color-white; - height: 20px; - width: 20px; - } } - span { + & .content { + color: $color-white; + display: flex; + align-items: center; + justify-content: center; font-size: $fs15; } } - - &.fixed { - position: fixed; - } - - &.error { - background-color: $color-danger; - } - - &.success { - background-color: $color-success; - } - - &.warning { - background-color: $color-warning; - } - - &.info { - background-color: $color-info; - } - - &.quick { - .btn-close { - display: none; - } - } - - &.hide { - @include animation(0, .6s, fadeOutUp); - } } -.inline-banner { - display: flex; - margin-bottom: $big; +.banner.floating, +.banner.inline { min-height: 40px; - width: 100%; - .icon { + & .wrapper { display: flex; - flex-shrink: 0; - justify-content: center; - margin-right: $small; - padding: $small; - width: 40px; - svg { - fill: $color-white; - height: 20px; - width: 20px; + & .icon { + padding: $small; + width: 40px; } - } - .content { - display: flex; - flex-direction: column; - } - - .main { - display: flex; - } - - .extra { - display: flex; - justify-content: flex-end; - padding: $small; - - > div:not(:last-child) { - margin-right: $small; + & .content { + color: $color-black; + display: flex; + font-size: $fs14; + padding: $small; + width: 100%; } } - .text { - display: flex; - font-size: $fs14; - color: $color-black; - padding: $small; - } - &.error { - background-color: lighten($color-danger,30%); - .icon { - background-color: $color-danger; + & .content { + background-color: lighten($color-danger,30%); } } &.success { - background-color: lighten($color-success,30%); - .icon { - background-color: $color-success; + & .content { + background-color: lighten($color-success,30%); } } &.warning { - background-color: lighten($color-warning,30%); - .icon { - background-color: $color-warning; + & .content { + background-color: lighten($color-warning,30%); } } &.info { - background-color: lighten($color-info,30%); - .icon { - background-color: $color-info; - } - } - - .btn-close { - width: 40px; - height: 40px; - flex-shrink: 0; - display: flex; - justify-content: center; - align-items: center; - cursor: pointer; - opacity: .35; - - svg { - fill: $color-black; - height: 18px; - width: 18px; - transform: rotate(45deg); - } - - &:hover { - opacity: .8; - } - } - - &.quick { - .btn-close { - display: none; + & .content { + background-color: lighten($color-info,30%); } } } +.banner.floating { + box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.18); + position: absolute; + top: 70px; + left: 0; + right: 0; + width: 35rem; + margin-left: auto; + margin-right: auto; + z-index: 20; + + &.error { + border: 1px solid $color-danger; + } + + &.success { + border: 1px solid $color-success; + } + + &.warning { + border: 1px solid $color-warning; + } + + &.info { + border: 1px solid $color-info; + } +} + +.banner.inline { + width: 100%; + margin-bottom: $big; +} + +// .inline-banner { +// // display: flex; +// // margin-bottom: $big; +// // min-height: 40px; +// // width: 100%; +// +// // .icon { +// // display: flex; +// // flex-shrink: 0; +// // justify-content: center; +// // margin-right: $small; +// // padding: $small; +// // width: 40px; +// // +// // svg { +// // fill: $color-white; +// // height: 20px; +// // width: 20px; +// // } +// // } +// +// .content { +// display: flex; +// flex-direction: column; +// } +// +// .main { +// display: flex; +// } +// +// .extra { +// display: flex; +// justify-content: flex-end; +// padding: $small; +// +// > div:not(:last-child) { +// margin-right: $small; +// } +// } +// +// .text { +// display: flex; +// font-size: $fs14; +// color: $color-black; +// padding: $small; +// } +// +// &.error { +// background-color: lighten($color-danger,30%); +// .icon { +// background-color: $color-danger; +// } +// } +// +// &.success { +// background-color: lighten($color-success,30%); +// .icon { +// background-color: $color-success; +// } +// } +// +// &.warning { +// background-color: lighten($color-warning,30%); +// .icon { +// background-color: $color-warning; +// } +// } +// +// &.info { +// background-color: lighten($color-info,30%); +// .icon { +// background-color: $color-info; +// } +// } +// +// .btn-close { +// width: 40px; +// height: 40px; +// flex-shrink: 0; +// display: flex; +// justify-content: center; +// align-items: center; +// cursor: pointer; +// opacity: .35; +// +// svg { +// fill: $color-black; +// height: 18px; +// width: 18px; +// transform: rotate(45deg); +// } +// +// &:hover { +// opacity: .8; +// } +// } +// +// &.quick { +// .btn-close { +// display: none; +// } +// } +// } + .close-bezier { fill: $color-danger; stroke: $color-danger-dark; diff --git a/frontend/src/app/main/data/messages.cljs b/frontend/src/app/main/data/messages.cljs index aaeb60464..51420154c 100644 --- a/frontend/src/app/main/data/messages.cljs +++ b/frontend/src/app/main/data/messages.cljs @@ -23,6 +23,15 @@ (def +animation-timeout+ 600) +(s/def ::message-type #{:success :error :info :warning}) +(s/def ::message-position #{:fixed :floating :inline}) +(s/def ::message-status #{:visible :hide}) +(s/def ::message-controls #{:none :close :inline-actions :bottom-actions}) +(s/def ::label string?) +(s/def ::callback fn?) +(s/def ::message-action (s/keys :req-un [::label ::callback])) +(s/def ::message-actions (s/nilable (s/coll-of ::message-action :kind vector?))) + (defn show [data] (ptk/reify ::show @@ -79,3 +88,11 @@ (show {:content content :type :warning :timeout timeout}))) + +(defn info-dialog + [content controls actions] + (show {:content content + :type :info + :controls controls + :actions actions})) + diff --git a/frontend/src/app/main/data/workspace/notifications.cljs b/frontend/src/app/main/data/workspace/notifications.cljs index 20b851491..eea454c78 100644 --- a/frontend/src/app/main/data/workspace/notifications.cljs +++ b/frontend/src/app/main/data/workspace/notifications.cljs @@ -13,6 +13,7 @@ [app.common.geom.point :as gpt] [app.common.pages :as cp] [app.common.spec :as us] + [app.main.data.messages :as dm] [app.main.data.workspace.common :as dwc] [app.main.data.workspace.persistence :as dwp] [app.main.data.workspace.libraries :as dwl] @@ -23,6 +24,7 @@ [app.util.time :as dt] [app.util.transit :as t] [app.util.websockets :as ws] + [app.util.i18n :as i18n :refer [tr]] [beicon.core :as rx] [cljs.spec.alpha :as s] [clojure.set :as set] @@ -211,6 +213,16 @@ ptk/WatchEvent (watch [_ state stream] (when (contains? (:workspace-libraries state) file-id) - (rx/of (dwl/ext-library-changed file-id changes) - (dwl/sync-file file-id)))))) + (let [do-update #(do + (st/emit! (dwl/sync-file file-id)) + (st/emit! dm/hide)) + do-dismiss #(st/emit! dm/hide)] + (rx/of (dwl/ext-library-changed file-id changes) + (dm/info-dialog + (tr "workspace.updates.there-are-updates") + :inline-actions + [{:label (tr "workspace.updates.update") + :callback do-update} + {:label (tr "workspace.updates.dismiss") + :callback do-dismiss}]))))))) diff --git a/frontend/src/app/main/ui/messages.cljs b/frontend/src/app/main/ui/messages.cljs index 60e4eaf71..a4310941c 100644 --- a/frontend/src/app/main/ui/messages.cljs +++ b/frontend/src/app/main/ui/messages.cljs @@ -10,6 +10,9 @@ (ns app.main.ui.messages (:require [rumext.alpha :as mf] + [clojure.spec.alpha :as s] + [app.common.uuid :as uuid] + [app.common.spec :as us] [app.main.ui.icons :as i] [app.main.data.messages :as dm] [app.main.refs :as refs] @@ -19,57 +22,69 @@ [app.util.i18n :as i18n :refer [t]] [app.util.timers :as ts])) -(defn- type->icon - [type] - (case type - :warning i/msg-warning - :error i/msg-error - :success i/msg-success - :info i/msg-info - i/msg-error)) - -(mf/defc notification-item - [{:keys [type status on-close quick? content] :as props}] - (let [klass (dom/classnames - :fixed true - :success (= type :success) - :error (= type :error) - :info (= type :info) - :warning (= type :warning) - :hide (= status :hide) - :quick quick?)] - [:section.banner {:class klass} - [:div.content - [:div.icon (type->icon type)] - [:span content]] - [:div.btn-close {:on-click on-close} i/close]])) +(mf/defc banner + [{:keys [type position status controls content actions on-close] :as props}] + (us/assert ::dm/message-type type) + (us/assert ::dm/message-position position) + (us/assert ::dm/message-status status) + (us/assert ::dm/message-controls controls) + (us/assert ::dm/message-actions actions) + (us/assert (s/nilable ::us/fn) on-close) + [:div.banner {:class (dom/classnames + :warning (= type :warning) + :error (= type :error) + :success (= type :success) + :info (= type :info) + :fixed (= position :fixed) + :floating (= position :floating) + :inline (= position :inline) + :hide (= status :hide))} + [:div.wrapper + [:div.icon (case type + :warning i/msg-warning + :error i/msg-error + :success i/msg-success + :info i/msg-info + i/msg-error)] + [:div.content {:class (dom/classnames + :inline-actions (= controls :inline-actions) + :bottom-actions (= controls :bottom-actions))} + content + (when (or (= controls :bottom-actions) (= controls :inline-actions)) + [:div.actions + (for [action actions] + [:div.btn-secondary.btn-small {:key (uuid/next) + :on-click (:callback action)} + (:label action)])])] + (when (= controls :close) + [:div.btn-close {:on-click on-close} i/close])]]) (mf/defc notifications [] (let [message (mf/deref refs/message) on-close #(st/emit! dm/hide)] (when message - [:& notification-item {:type (:type message) - :quick? (boolean (:timeout message)) - :status (:status message) - :content (:content message) - :on-close on-close}]))) + [:& banner (assoc message + :position :floating + :controls (if (some? (:controls message)) + (:controls message) + (if (some? (:timeout message)) + :none + :close)) + :on-close on-close)]))) (mf/defc inline-banner {::mf/wrap [mf/memo]} - [{:keys [type on-close content children] :as props}] - [:div.inline-banner {:class (dom/classnames - :warning (= type :warning) - :error (= type :error) - :success (= type :success) - :info (= type :info) - :quick (not on-close))} - [:div.icon (type->icon type)] - [:div.content - [:div.main - [:span.text content] - [:div.btn-close {:on-click on-close} i/close]] - (when children - [:div.extra - children])]]) + [{:keys [type content on-close actions] :as props}] + [:& banner {:type type + :position :inline + :status :visible + :controls (if (some? on-close) + :close + (if (some? actions) + :bottom-actions + :none)) + :content content + :on-close on-close + :actions actions}]) diff --git a/frontend/src/app/main/ui/settings/profile.cljs b/frontend/src/app/main/ui/settings/profile.cljs index bcee7b8c1..b391e15ee 100644 --- a/frontend/src/app/main/ui/settings/profile.cljs +++ b/frontend/src/app/main/ui/settings/profile.cljs @@ -75,10 +75,12 @@ (not= (:pending-email prof) (:email prof)) [:& msgs/inline-banner {:type :info - :content (t locale "settings.change-email-info3" (:pending-email prof))} - [:div.btn-secondary.btn-small - {:on-click #(st/emit! udu/cancel-email-change)} - (t locale "settings.cancel-email-change")]] + :content (t locale "settings.change-email-info3" (:pending-email prof)) + :actions [{:label (t locale "settings.cancel-email-change") + :callback #(st/emit! udu/cancel-email-change)}]}] + ;; [:div.btn-secondary.btn-small + ;; {:on-click #(st/emit! udu/cancel-email-change)} + ;; (t locale "settings.cancel-email-change")]] :else [:& msgs/inline-banner