From b3252ec2b25c2ae7ef3a7904028ad4f7942f10d9 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Fri, 25 Sep 2020 14:51:21 +0200 Subject: [PATCH] :recycle: Refactor dashboard (add teams) --- backend/src/app/db.clj | 29 +- backend/src/app/migrations.clj | 10 + .../sql/0002-add-profile-tables.sql | 1 - .../sql/0003-add-project-tables.sql | 3 - ...028-add-team-project-profile-rel-table.sql | 12 + .../0029-del-project-profile-rel-indexes.sql | 4 + .../0030-mod-file-table-add-missing-index.sql | 1 + .../src/app/services/mutations/projects.clj | 32 +- backend/src/app/services/mutations/teams.clj | 16 +- backend/src/app/services/queries/profile.clj | 12 +- backend/src/app/services/queries/projects.clj | 31 +- .../src/app/services/queries/recent_files.clj | 34 +- backend/src/app/services/queries/teams.clj | 52 ++ frontend/resources/locales.json | 14 +- .../styles/common/dependencies/colors.scss | 1 + .../styles/common/dependencies/helpers.scss | 1 + .../resources/styles/common/framework.scss | 12 +- frontend/resources/styles/main-default.scss | 20 +- .../styles/main/layouts/libraries-page.scss | 6 - .../styles/main/layouts/main-layout.scss | 15 +- .../styles/main/layouts/projects-page.scss | 6 - .../main/layouts/recent-files-page.scss | 41 -- .../styles/main/layouts/search-page.scss | 6 - .../styles/main/partials/activity-bar.scss | 6 +- .../styles/main/partials/color-palette.scss | 6 +- .../styles/main/partials/context-menu.scss | 13 +- .../styles/main/partials/dashboard-grid.scss | 246 +++----- .../main/partials/dashboard-header.scss | 100 ++++ .../main/partials/dashboard-sidebar.scss | 481 ++++++++++++---- .../styles/main/partials/dashboard.scss | 76 +++ .../resources/styles/main/partials/login.scss | 0 .../styles/main/partials/main-bar.scss | 167 ------ .../resources/styles/main/partials/modal.scss | 2 +- frontend/src/app/main.cljs | 3 +- frontend/src/app/main/data/dashboard.cljs | 373 +++++-------- .../app/main/data/workspace/persistence.cljs | 2 +- frontend/src/app/main/store.cljs | 4 + frontend/src/app/main/ui.cljs | 14 +- frontend/src/app/main/ui/dashboard.cljs | 112 ++-- .../src/app/main/ui/dashboard/common.cljs | 58 -- frontend/src/app/main/ui/dashboard/files.cljs | 126 +++++ frontend/src/app/main/ui/dashboard/grid.cljs | 91 ++- .../src/app/main/ui/dashboard/libraries.cljs | 47 +- .../src/app/main/ui/dashboard/profile.cljs | 57 -- .../src/app/main/ui/dashboard/project.cljs | 85 --- .../src/app/main/ui/dashboard/projects.cljs | 138 +++++ .../app/main/ui/dashboard/recent_files.cljs | 95 ---- .../src/app/main/ui/dashboard/search.cljs | 61 +- .../src/app/main/ui/dashboard/sidebar.cljs | 526 ++++++++++++------ frontend/src/app/main/ui/settings.cljs | 1 - .../src/app/main/ui/workspace/header.cljs | 2 +- frontend/src/app/util/object.cljs | 12 +- 52 files changed, 1842 insertions(+), 1421 deletions(-) create mode 100644 backend/src/app/migrations/sql/0028-add-team-project-profile-rel-table.sql create mode 100644 backend/src/app/migrations/sql/0029-del-project-profile-rel-indexes.sql create mode 100644 backend/src/app/migrations/sql/0030-mod-file-table-add-missing-index.sql delete mode 100644 frontend/resources/styles/main/layouts/libraries-page.scss delete mode 100644 frontend/resources/styles/main/layouts/projects-page.scss delete mode 100644 frontend/resources/styles/main/layouts/recent-files-page.scss delete mode 100644 frontend/resources/styles/main/layouts/search-page.scss create mode 100644 frontend/resources/styles/main/partials/dashboard-header.scss create mode 100644 frontend/resources/styles/main/partials/dashboard.scss delete mode 100644 frontend/resources/styles/main/partials/login.scss delete mode 100644 frontend/resources/styles/main/partials/main-bar.scss delete mode 100644 frontend/src/app/main/ui/dashboard/common.cljs create mode 100644 frontend/src/app/main/ui/dashboard/files.cljs delete mode 100644 frontend/src/app/main/ui/dashboard/profile.cljs delete mode 100644 frontend/src/app/main/ui/dashboard/project.cljs create mode 100644 frontend/src/app/main/ui/dashboard/projects.cljs delete mode 100644 frontend/src/app/main/ui/dashboard/recent_files.cljs diff --git a/backend/src/app/db.clj b/backend/src/app/db.clj index 78016e29d..777a25d1b 100644 --- a/backend/src/app/db.clj +++ b/backend/src/app/db.clj @@ -2,12 +2,21 @@ ;; 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) 2019 Andrey Antukh +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 UXBOX Labs SL (ns app.db (:require - [clojure.spec.alpha :as s] + [app.common.exceptions :as ex] + [app.config :as cfg] + [app.metrics :as mtx] + [app.util.data :as data] + [app.util.time :as dt] + [app.util.transit :as t] [clojure.data.json :as json] + [clojure.spec.alpha :as s] [clojure.string :as str] [clojure.tools.logging :as log] [lambdaisland.uri :refer [uri]] @@ -17,19 +26,13 @@ [next.jdbc.optional :as jdbc-opt] [next.jdbc.result-set :as jdbc-rs] [next.jdbc.sql :as jdbc-sql] - [next.jdbc.sql.builder :as jdbc-bld] - [app.common.exceptions :as ex] - [app.config :as cfg] - [app.metrics :as mtx] - [app.util.time :as dt] - [app.util.transit :as t] - [app.util.data :as data]) + [next.jdbc.sql.builder :as jdbc-bld]) (:import - org.postgresql.util.PGobject - org.postgresql.util.PGInterval - com.zaxxer.hikari.metrics.prometheus.PrometheusMetricsTrackerFactory com.zaxxer.hikari.HikariConfig - com.zaxxer.hikari.HikariDataSource)) + com.zaxxer.hikari.HikariDataSource + com.zaxxer.hikari.metrics.prometheus.PrometheusMetricsTrackerFactory + org.postgresql.util.PGInterval + org.postgresql.util.PGobject)) (def initsql (str "SET statement_timeout = 10000;\n" diff --git a/backend/src/app/migrations.clj b/backend/src/app/migrations.clj index 920ad7d27..b384b0e6c 100644 --- a/backend/src/app/migrations.clj +++ b/backend/src/app/migrations.clj @@ -98,6 +98,16 @@ {:name "0027-mod-file-table-ignore-sync" :fn (mg/resource "app/migrations/sql/0027-mod-file-table-ignore-sync.sql")} + + {:name "0028-add-team-project-profile-rel-table" + :fn (mg/resource "app/migrations/sql/0028-add-team-project-profile-rel-table.sql")} + + {:name "0029-del-project-profile-rel-indexes" + :fn (mg/resource "app/migrations/sql/0029-del-project-profile-rel-indexes.sql")} + + {:name "0030-mod-file-table-add-missing-index" + :fn (mg/resource "app/migrations/sql/0030-mod-file-table-add-missing-index.sql")} + ]}) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/backend/src/app/migrations/sql/0002-add-profile-tables.sql b/backend/src/app/migrations/sql/0002-add-profile-tables.sql index 230a35a5b..1e8e4d749 100644 --- a/backend/src/app/migrations/sql/0002-add-profile-tables.sql +++ b/backend/src/app/migrations/sql/0002-add-profile-tables.sql @@ -78,7 +78,6 @@ VALUES ('00000000-0000-0000-0000-000000000000'::uuid, true); - CREATE TABLE team_profile_rel ( team_id uuid NOT NULL REFERENCES team(id) ON DELETE CASCADE, profile_id uuid NOT NULL REFERENCES profile(id) ON DELETE RESTRICT, diff --git a/backend/src/app/migrations/sql/0003-add-project-tables.sql b/backend/src/app/migrations/sql/0003-add-project-tables.sql index 9ba826010..925f7c8b0 100644 --- a/backend/src/app/migrations/sql/0003-add-project-tables.sql +++ b/backend/src/app/migrations/sql/0003-add-project-tables.sql @@ -28,9 +28,6 @@ CREATE TABLE project_profile_rel ( PRIMARY KEY (profile_id, project_id) ); -COMMENT ON TABLE project_profile_rel - IS 'Relation between projects and profiles (NM)'; - CREATE INDEX project_profile_rel__profile_id__idx ON project_profile_rel(profile_id); diff --git a/backend/src/app/migrations/sql/0028-add-team-project-profile-rel-table.sql b/backend/src/app/migrations/sql/0028-add-team-project-profile-rel-table.sql new file mode 100644 index 000000000..a2490b1cc --- /dev/null +++ b/backend/src/app/migrations/sql/0028-add-team-project-profile-rel-table.sql @@ -0,0 +1,12 @@ +CREATE TABLE team_project_profile_rel ( + team_id uuid NOT NULL REFERENCES team(id) ON DELETE CASCADE, + profile_id uuid NOT NULL REFERENCES profile(id) ON DELETE CASCADE, + project_id uuid NOT NULL REFERENCES project(id) ON DELETE CASCADE, + + created_at timestamptz NOT NULL DEFAULT clock_timestamp(), + modified_at timestamptz NOT NULL DEFAULT clock_timestamp(), + + is_pinned boolean NOT NULL DEFAULT false, + + PRIMARY KEY (team_id, profile_id, project_id) +); diff --git a/backend/src/app/migrations/sql/0029-del-project-profile-rel-indexes.sql b/backend/src/app/migrations/sql/0029-del-project-profile-rel-indexes.sql new file mode 100644 index 000000000..7a35640ac --- /dev/null +++ b/backend/src/app/migrations/sql/0029-del-project-profile-rel-indexes.sql @@ -0,0 +1,4 @@ +--- Drop duplicate indexes + +DROP INDEX IF EXISTS project_profile_rel__project_id__idx; +DROP INDEX IF EXISTS project_profile_rel__profile_id__idx; diff --git a/backend/src/app/migrations/sql/0030-mod-file-table-add-missing-index.sql b/backend/src/app/migrations/sql/0030-mod-file-table-add-missing-index.sql new file mode 100644 index 000000000..5c940bee7 --- /dev/null +++ b/backend/src/app/migrations/sql/0030-mod-file-table-add-missing-index.sql @@ -0,0 +1 @@ +CREATE INDEX IF NOT EXISTS file__project_id__idx ON file (project_id); diff --git a/backend/src/app/services/mutations/projects.clj b/backend/src/app/services/mutations/projects.clj index 1b7d42b38..74f648e70 100644 --- a/backend/src/app/services/mutations/projects.clj +++ b/backend/src/app/services/mutations/projects.clj @@ -61,6 +61,7 @@ (declare create-project) (declare create-project-profile) +(declare create-team-project-profile) (s/def ::team-id ::us/uuid) (s/def ::create-project @@ -70,9 +71,11 @@ (sm/defmutation ::create-project [params] (db/with-atomic [conn db/pool] - (let [proj (create-project conn params)] - (create-project-profile conn (assoc params :project-id (:id proj))) - proj))) + (let [proj (create-project conn params) + params (assoc params :project-id (:id proj))] + (create-project-profile conn params) + (create-team-project-profile conn params) + (assoc proj :is-pinned true)))) (defn create-project [conn {:keys [id profile-id team-id name default?] :as params}] @@ -93,8 +96,31 @@ :is-admin true :can-edit true})) +(defn create-team-project-profile + [conn {:keys [team-id project-id profile-id] :as params}] + (db/insert! conn :team-project-profile-rel + {:project-id project-id + :profile-id profile-id + :team-id team-id + :is-pinned true})) +;; --- Mutation: Toggle Project Pin + +(s/def ::is-pinned ::us/boolean) +(s/def ::toggle-project-pin + (s/keys :req-un [::profile-id ::id ::team-id ::is-pinned])) + +(sm/defmutation ::toggle-project-pin + [{:keys [id profile-id team-id is-pinned] :as params}] + (db/with-atomic [conn db/pool] + (db/update! conn :team-project-profile-rel + {:is-pinned is-pinned} + {:profile-id profile-id + :project-id id + :team-id team-id}) + nil)) + ;; --- Mutation: Rename Project (declare rename-project) diff --git a/backend/src/app/services/mutations/teams.clj b/backend/src/app/services/mutations/teams.clj index 614d2496d..b7615592b 100644 --- a/backend/src/app/services/mutations/teams.clj +++ b/backend/src/app/services/mutations/teams.clj @@ -15,6 +15,7 @@ [app.common.uuid :as uuid] [app.db :as db] [app.services.mutations :as sm] + [app.services.mutations.projects :as projects] [app.util.blob :as blob])) ;; --- Helpers & Specs @@ -27,6 +28,7 @@ (declare create-team) (declare create-team-profile) +(declare create-team-default-project) (s/def ::create-team (s/keys :req-un [::profile-id ::name] @@ -35,8 +37,10 @@ (sm/defmutation ::create-team [params] (db/with-atomic [conn db/pool] - (let [team (create-team conn params)] - (create-team-profile conn (assoc params :team-id (:id team))) + (let [team (create-team conn params) + params (assoc params :team-id (:id team))] + (create-team-profile conn params) + (create-team-default-project conn params) team))) (defn create-team @@ -57,3 +61,11 @@ :is-owner true :is-admin true :can-edit true})) + +(defn create-team-default-project + [conn {:keys [team-id profile-id] :as params}] + (let [proj (projects/create-project conn {:team-id team-id + :name "Drafts" + :default? true})] + (projects/create-project-profile conn {:project-id (:id proj) + :profile-id profile-id}))) diff --git a/backend/src/app/services/queries/profile.clj b/backend/src/app/services/queries/profile.clj index 5ffd10cef..cbbee35b6 100644 --- a/backend/src/app/services/queries/profile.clj +++ b/backend/src/app/services/queries/profile.clj @@ -53,16 +53,16 @@ (def ^:private sql:default-team-and-project "select t.id from team as t - inner join team_profile_rel as tpr on (tpr.team_id = t.id) - where tpr.profile_id = ? - and tpr.is_owner is true + inner join team_profile_rel as tp on (tp.team_id = t.id) + where tp.profile_id = ? + and tp.is_owner is true and t.is_default is true union all select p.id from project as p - inner join project_profile_rel as tpr on (tpr.project_id = p.id) - where tpr.profile_id = ? - and tpr.is_owner is true + inner join project_profile_rel as tp on (tp.project_id = p.id) + where tp.profile_id = ? + and tp.is_owner is true and p.is_default is true") (defn retrieve-additional-data diff --git a/backend/src/app/services/queries/projects.clj b/backend/src/app/services/queries/projects.clj index 688549d38..2391746a7 100644 --- a/backend/src/app/services/queries/projects.clj +++ b/backend/src/app/services/queries/projects.clj @@ -60,7 +60,6 @@ (s/def ::team-id ::us/uuid) (s/def ::profile-id ::us/uuid) - (s/def ::projects (s/keys :req-un [::profile-id ::team-id])) @@ -68,31 +67,37 @@ [{:keys [profile-id team-id]}] (with-open [conn (db/open)] (teams/check-read-permissions! conn profile-id team-id) - (retrieve-projects conn team-id))) + (retrieve-projects conn profile-id team-id))) (def sql:projects "select p.*, + tpp.is_pinned, (select count(*) from file as f - where f.project_id = p.id - and deleted_at is null) + where f.project_id = p.id + and deleted_at is null) as count from project as p + left join team_project_profile_rel as tpp + on (tpp.project_id = p.id and + tpp.team_id = p.team_id and + tpp.profile_id = ?) where p.team_id = ? and p.deleted_at is null order by p.modified_at desc") (defn retrieve-projects - [conn team-id] - (db/exec! conn [sql:projects team-id])) + [conn profile-id team-id] + (db/exec! conn [sql:projects profile-id team-id])) -;; --- Query: Projec by ID -(s/def ::project-id ::us/uuid) -(s/def ::project-by-id - (s/keys :req-un [::profile-id ::project-id])) +;; --- Query: Project -(sq/defquery ::project-by-id - [{:keys [profile-id project-id]}] +(s/def ::id ::us/uuid) +(s/def ::project + (s/keys :req-un [::profile-id ::id])) + +(sq/defquery ::project + [{:keys [profile-id id]}] (with-open [conn (db/open)] - (let [project (db/get-by-id conn :project project-id)] + (let [project (db/get-by-id conn :project id)] (check-edition-permissions! conn profile-id project) project))) diff --git a/backend/src/app/services/queries/recent_files.clj b/backend/src/app/services/queries/recent_files.clj index 6e16dacc8..26937821b 100644 --- a/backend/src/app/services/queries/recent_files.clj +++ b/backend/src/app/services/queries/recent_files.clj @@ -18,19 +18,17 @@ [app.services.queries.projects :as projects :refer [retrieve-projects]] [app.services.queries.files :refer [decode-row-xf]])) -(def sql:project-recent-files - "select f.* - from file as f - where f.project_id = ? - and f.deleted_at is null - order by f.modified_at desc - limit 5") - -(defn recent-by-project - [conn profile-id project] - (let [project-id (:id project)] - (projects/check-edition-permissions! conn profile-id project) - (into [] decode-row-xf (db/exec! conn [sql:project-recent-files project-id])))) +(def sql:recent-files + "with recent_files as ( + select f.*, row_number() over w as row_num + from file as f + join project as p on (p.id = f.project_id) + where p.team_id = ? + and p.deleted_at is null + window w as (partition by f.project_id order by f.modified_at desc) + order by f.modified_at desc + ) + select * from recent_files where row_num <= 6;") (s/def ::team-id ::us/uuid) (s/def ::profile-id ::us/uuid) @@ -42,9 +40,7 @@ [{:keys [profile-id team-id]}] (with-open [conn (db/open)] (teams/check-read-permissions! conn profile-id team-id) - (->> (retrieve-projects conn team-id) - ;; Retrieve for each proyect the 5 more recent files - (map (partial recent-by-project conn profile-id)) - ;; Change the structure so it's a map with project-id as keys - (flatten) - (group-by :project-id)))) + (let [files (db/exec! conn [sql:recent-files team-id])] + (into [] decode-row-xf files)))) + + diff --git a/backend/src/app/services/queries/teams.clj b/backend/src/app/services/queries/teams.clj index 4d86477e1..5efb6ee63 100644 --- a/backend/src/app/services/queries/teams.clj +++ b/backend/src/app/services/queries/teams.clj @@ -15,6 +15,7 @@ [app.common.uuid :as uuid] [app.db :as db] [app.services.queries :as sq] + [app.services.queries.profile :as profile] [app.util.blob :as blob])) ;; --- Team Edition Permissions @@ -43,3 +44,54 @@ (when-not row (ex/raise :type :validation :code :not-authorized)))) + + +;; --- Query: Teams + +(declare retrieve-teams) + +(s/def ::profile-id ::us/uuid) +(s/def ::teams + (s/keys :req-un [::profile-id])) + +(sq/defquery ::teams + [{:keys [profile-id]}] + (with-open [conn (db/open)] + (retrieve-teams conn profile-id))) + +(def sql:teams + "select t.*, + tp.is_owner, + tp.is_admin, + tp.can_edit, + (t.id = ?) as is_default + from team_profile_rel as tp + join team as t on (t.id = tp.team_id) + where t.deleted_at is null + and tp.profile_id = ? + order by t.created_at asc") + +(defn retrieve-teams + [conn profile-id] + (let [defaults (profile/retrieve-additional-data conn profile-id)] + (db/exec! conn [sql:teams (:default-team-id defaults) profile-id]))) + +;; --- Query: Projec by ID + +(declare retrieve-team-projects) +(declare retrieve-team) + +(s/def ::id ::us/uuid) +(s/def ::team + (s/keys :req-un [::profile-id ::id])) + +(sq/defquery ::team + [{:keys [profile-id id]}] + (with-open [conn (db/open)] + (retrieve-team conn profile-id id))) + +(defn- retrieve-team + [conn profile-id team-id] + (let [defaults (profile/retrieve-additional-data conn profile-id) + sql (str "WITH teams AS (" sql:teams ") SELECT * FROM teams WHERE id=?")] + (db/exec-one! conn [sql (:default-team-id defaults) profile-id team-id]))) diff --git a/frontend/resources/locales.json b/frontend/resources/locales.json index 04fd94fd0..206a6da85 100644 --- a/frontend/resources/locales.json +++ b/frontend/resources/locales.json @@ -410,15 +410,15 @@ "es" : "Bibliotecas Compartidas" } }, - "dashboard.header.new-file" : { - "used-in" : [ "src/app/main/ui/dashboard/project.cljs:71" ], + "dashboard.sidebar.projects" : { "translations" : { - "en" : "+ New file", - "fr" : "+ Nouveau fichier", - "ru" : "+ Новый файл", - "es" : "+ Nuevo archivo" + "en" : "Projects", + "fr" : "Projetes", + "ru" : "Проекты", + "es" : "Proyectos" } }, + "dashboard.header.new-project" : { "used-in" : [ "src/app/main/ui/dashboard/recent_files.cljs:46" ], "translations" : { @@ -689,7 +689,7 @@ }, "unused" : true }, - "ds.new-file" : { + "dashboard.new-file" : { "used-in" : [ "src/app/main/ui/dashboard/grid.cljs:179", "src/app/main/ui/dashboard/grid.cljs:191" ], "translations" : { "en" : "+ New File", diff --git a/frontend/resources/styles/common/dependencies/colors.scss b/frontend/resources/styles/common/dependencies/colors.scss index 0e8a21688..bd995bc8c 100644 --- a/frontend/resources/styles/common/dependencies/colors.scss +++ b/frontend/resources/styles/common/dependencies/colors.scss @@ -9,6 +9,7 @@ $color-white: #ffffff; $color-black: #000000; $color-canvas: #E8E9EA; +$color-dashboard: #F6F6F6; // Main color $color-primary: #31EFB8; diff --git a/frontend/resources/styles/common/dependencies/helpers.scss b/frontend/resources/styles/common/dependencies/helpers.scss index e23f58b39..5e5bb1db8 100644 --- a/frontend/resources/styles/common/dependencies/helpers.scss +++ b/frontend/resources/styles/common/dependencies/helpers.scss @@ -24,6 +24,7 @@ $size-6: 2rem; $br-small: 3px; $br-medium: 5px; $br-big: 8px; +$br-huge: 12px; // Alignments .text-left { diff --git a/frontend/resources/styles/common/framework.scss b/frontend/resources/styles/common/framework.scss index c19e4a8cc..9b9c66e78 100644 --- a/frontend/resources/styles/common/framework.scss +++ b/frontend/resources/styles/common/framework.scss @@ -49,7 +49,7 @@ .btn-secondary { @extend %btn; background: $color-white; - border: 1px solid $color-black; + border: 1px solid $color-gray-20; color: $color-black; &:hover { background: $color-primary; @@ -97,16 +97,18 @@ .btn-icon-light { @extend %btn; - background: $color-gray-10; - color: $color-gray-40; + background: $color-white; + border: 1px solid $color-gray-20; + color: $color-black; padding: $x-small; svg { - fill: $color-gray-40; + fill: $color-black; } &:hover { background: $color-primary; + border-color: $color-primary; svg { - fill: $color-gray-60; + fill: $color-black; } } } diff --git a/frontend/resources/styles/main-default.scss b/frontend/resources/styles/main-default.scss index 10410d673..23d46901c 100644 --- a/frontend/resources/styles/main-default.scss +++ b/frontend/resources/styles/main-default.scss @@ -2,10 +2,12 @@ // 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) 2016 Andrey Antukh -// Copyright (c) 2016 Juan de la Cruz +// This Source Code Form is "Incompatible With Secondary Licenses", as +// defined by the Mozilla Public License, v. 2.0. +// +// Copyright (c) 2020 UXBOX Labs SL -// app MAIN STYLES +// MAIN STYLES //################################################# // //################################################# @@ -25,10 +27,6 @@ @import 'common/base'; @import 'main/layouts/login'; @import 'main/layouts/main-layout'; -@import 'main/layouts/projects-page'; -@import 'main/layouts/libraries-page'; -@import 'main/layouts/recent-files-page'; -@import 'main/layouts/search-page'; @import "main/layouts/not-found"; @import "main/layouts/viewer"; @@ -42,7 +40,6 @@ // Partials //################################################# -@import "main/partials/login"; @import "main/partials/texts"; @import "main/partials/viewer"; @import "main/partials/viewer-header"; @@ -52,18 +49,20 @@ @import 'main/partials/color-palette'; @import 'main/partials/colorpicker'; @import 'main/partials/context-menu'; +@import 'main/partials/dashboard'; +@import 'main/partials/dashboard-header'; @import 'main/partials/dashboard-grid'; +@import 'main/partials/dashboard-sidebar'; @import 'main/partials/debug-icons-preview'; @import 'main/partials/editable-label'; @import 'main/partials/forms'; @import 'main/partials/left-toolbar'; -@import 'main/partials/dashboard-sidebar'; @import 'main/partials/loader'; -@import 'main/partials/main-bar'; @import 'main/partials/modal'; @import 'main/partials/project-bar'; @import 'main/partials/sidebar'; @import 'main/partials/sidebar-align-options'; +@import 'main/partials/sidebar-assets'; @import 'main/partials/sidebar-document-history'; @import 'main/partials/sidebar-element-options'; @import 'main/partials/sidebar-icons'; @@ -71,7 +70,6 @@ @import 'main/partials/sidebar-layers'; @import 'main/partials/sidebar-sitemap'; @import 'main/partials/sidebar-tools'; -@import 'main/partials/sidebar-assets'; @import 'main/partials/tab-container'; @import 'main/partials/tool-bar'; @import 'main/partials/user-settings'; diff --git a/frontend/resources/styles/main/layouts/libraries-page.scss b/frontend/resources/styles/main/layouts/libraries-page.scss deleted file mode 100644 index dc614aafc..000000000 --- a/frontend/resources/styles/main/layouts/libraries-page.scss +++ /dev/null @@ -1,6 +0,0 @@ -.libraries-page { - padding: 1rem; - background-color: $color-white; - flex: 1 0 0; - overflow-y: auto; -} diff --git a/frontend/resources/styles/main/layouts/main-layout.scss b/frontend/resources/styles/main/layouts/main-layout.scss index 0852beb6f..edabb7c48 100644 --- a/frontend/resources/styles/main/layouts/main-layout.scss +++ b/frontend/resources/styles/main/layouts/main-layout.scss @@ -2,8 +2,10 @@ // 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) 2015-2016 Andrey Antukh -// Copyright (c) 2015-2016 Juan de la Cruz +// This Source Code Form is "Incompatible With Secondary Licenses", as +// defined by the Mozilla Public License, v. 2.0. +// +// Copyright (c) 2020 UXBOX Labs SL .main-content { display: flex; @@ -14,23 +16,22 @@ .dashboard-layout { background-color: $color-white; display: grid; - grid-template-rows: 40px 1fr; + grid-template-rows: 50px 1fr; grid-template-columns: 40px 180px 1fr; height: 100vh; - & .dashboard-sidebar { - grid-row: 2; + .dashboard-sidebar { + grid-row: 1 / span 2; grid-column: 1 / span 2; overflow: hidden; } - & .dashboard-content { + .dashboard-content { grid-row: 1 / span 2; } } .dashboard-content { - background-color: lighten($color-gray-10, 5%); display: flex; flex-direction: column; } diff --git a/frontend/resources/styles/main/layouts/projects-page.scss b/frontend/resources/styles/main/layouts/projects-page.scss deleted file mode 100644 index 3b9394058..000000000 --- a/frontend/resources/styles/main/layouts/projects-page.scss +++ /dev/null @@ -1,6 +0,0 @@ -.projects-page { - padding: 1rem; - background-color: $color-white; - flex: 1 0 0; - overflow-y: auto; -} diff --git a/frontend/resources/styles/main/layouts/recent-files-page.scss b/frontend/resources/styles/main/layouts/recent-files-page.scss deleted file mode 100644 index b87226bb8..000000000 --- a/frontend/resources/styles/main/layouts/recent-files-page.scss +++ /dev/null @@ -1,41 +0,0 @@ -.recent-files-page { - background-color: $color-white; - flex: 1 0 0; - overflow-y: auto; -} - -.recent-files-row { - padding: 1rem; - border-top: 1px solid $color-gray-10; - - &.first { - border-top: none; - } -} - -.recent-files-row-title { - display: flex; - flex-direction: row; - margin-left: $medium; - margin-top: $medium; -} - -.recent-files-row-title-name, .recent-files-row-title-info { - font-size: 15px; - line-height: 1rem; - font-weight: unset; -} - -.recent-files-row-title-name { - color: black; - margin-right: $medium; -} - -.recent-files-row-title-info { - color: $color-gray-30 -} - -.recent-files-empty { - margin: 30px; - font-size: 20px -} diff --git a/frontend/resources/styles/main/layouts/search-page.scss b/frontend/resources/styles/main/layouts/search-page.scss deleted file mode 100644 index 37e17f2a9..000000000 --- a/frontend/resources/styles/main/layouts/search-page.scss +++ /dev/null @@ -1,6 +0,0 @@ -.search-page { - padding: 1rem; - background-color: $color-white; - flex: 1 0 0; - overflow-y: auto; -} diff --git a/frontend/resources/styles/main/partials/activity-bar.scss b/frontend/resources/styles/main/partials/activity-bar.scss index c4bd911c9..5948b738c 100644 --- a/frontend/resources/styles/main/partials/activity-bar.scss +++ b/frontend/resources/styles/main/partials/activity-bar.scss @@ -2,8 +2,10 @@ // 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) 2015-2016 Andrey Antukh -// Copyright (c) 2015-2016 Juan de la Cruz +// This Source Code Form is "Incompatible With Secondary Licenses", as +// defined by the Mozilla Public License, v. 2.0. +// +// Copyright (c) 2020 UXBOX Labs SL .activity-bar { background-color: $color-gray-50; diff --git a/frontend/resources/styles/main/partials/color-palette.scss b/frontend/resources/styles/main/partials/color-palette.scss index 900915628..c4a702eee 100644 --- a/frontend/resources/styles/main/partials/color-palette.scss +++ b/frontend/resources/styles/main/partials/color-palette.scss @@ -2,8 +2,10 @@ // 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) 2015-2016 Andrey Antukh -// Copyright (c) 2015-2016 Juan de la Cruz +// This Source Code Form is "Incompatible With Secondary Licenses", as +// defined by the Mozilla Public License, v. 2.0. +// +// Copyright (c) 2020 UXBOX Labs SL .color-palette { @include animation(0,.5s,fadeInUp); diff --git a/frontend/resources/styles/main/partials/context-menu.scss b/frontend/resources/styles/main/partials/context-menu.scss index 9bff5e57f..8d2e7cdca 100644 --- a/frontend/resources/styles/main/partials/context-menu.scss +++ b/frontend/resources/styles/main/partials/context-menu.scss @@ -1,3 +1,12 @@ +// 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/. +// +// This Source Code Form is "Incompatible With Secondary Licenses", as +// defined by the Mozilla Public License, v. 2.0. +// +// Copyright (c) 2020 UXBOX Labs SL + .context-menu { position: relative; visibility: hidden; @@ -35,7 +44,7 @@ &:hover { color: $color-black; background-color: $color-primary-lighter; - } + } } .context-menu.is-selectable { @@ -49,6 +58,6 @@ background-position: 5% 48%; background-size: 10px; font-weight: bold; - } + } } diff --git a/frontend/resources/styles/main/partials/dashboard-grid.scss b/frontend/resources/styles/main/partials/dashboard-grid.scss index 0ed99bda1..cedf415fd 100644 --- a/frontend/resources/styles/main/partials/dashboard-grid.scss +++ b/frontend/resources/styles/main/partials/dashboard-grid.scss @@ -2,74 +2,23 @@ // 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) 2015-2016 Andrey Antukh -// Copyright (c) 2015-2016 Juan de la Cruz +// This Source Code Form is "Incompatible With Secondary Licenses", as +// defined by the Mozilla Public License, v. 2.0. +// +// Copyright (c) 2020 UXBOX Labs SL .dashboard-grid { font-size: $fs14; - .dashboard-title { - position: relative; - width: 100%; - - h2 { - text-align: center; - width: 100%; - .edit { - padding: 5px 10px; - background: $color-gray-50; - border: none; - height: 100%; - } - .close { - padding: 5px 10px; - background: $color-gray-50; - cursor: pointer; - svg { - transform: rotate(45deg); - fill: $color-gray-30; - height: 20px; - width: 20px; - } - } - } - - .edition { - align-items: center; - display: flex; - position: absolute; - right: 40px; - top: 0; - - span { - cursor: pointer; - - svg { - fill: $color-gray-30; - height: 20px; - margin: 0 10px; - width: 20px; - } - - &:hover { - - svg { - fill: $color-gray-50; - } - - } - - } - - } - - } - - .dashboard-grid-row { + .grid-row { display: flex; flex-wrap: wrap; width: 100%; align-content: flex-start; + + &.no-wrap { + flex-wrap: nowrap; + } } .grid-item { @@ -82,14 +31,37 @@ flex-shrink: 0; height: 200px; margin: $medium; - max-width: 300px; + max-width: 260px; min-width: 260px; position: relative; text-align: center; width: 18%; box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1); - & .overlay { + &.placeholder { + min-width: 115px; + max-width: 115px; + + display: flex; + flex-direction: column; + justify-content: center; + + .placeholder-icon { + svg { + transform: rotate(-90deg); + width: 20px; + height: 20px; + fill: $color-gray-30; + } + } + + .placeholder-label { + font-size: $fs14; + } + + } + + &.overlay { border-radius: 4px; border: 2px solid $color-primary; height: 100%; @@ -347,128 +319,52 @@ padding: $medium; } -} - -.grid-item-th { - background-position: center; - background-size: auto 80%; - background-repeat: no-repeat; - border-top-left-radius: $br-small; - border-top-right-radius: $br-small; - height: 70%; - overflow: hidden; - position: relative; - width: 100%; - - background-color: $color-canvas; - - .img-th { - height: auto; + .grid-item-th { + background-position: center; + background-size: auto 80%; + background-repeat: no-repeat; + border-top-left-radius: $br-small; + border-top-right-radius: $br-small; + height: 70%; + overflow: hidden; + position: relative; width: 100%; - } - svg { - height: 100%; + background-color: $color-canvas; + + .img-th { + height: auto; width: 100%; - } - -} - -// MULTISELECT OPTIONS BAR -.multiselect-bar { - @include animation(0,.5s,fadeInUp); - align-items: center; - background-color: $color-gray-50; - display: flex; - left: 0; - justify-content: center; - padding: $medium; - position: absolute; - width: 100%; - bottom: 0; - - .multiselect-nav { - align-items: center; - display: flex; - justify-content: center; - margin-left: 10%; - width: 110px; - - span { - margin-right: 1.5rem; - &:last-child { - margin-right: 0; - } } svg { - cursor: pointer; - fill: $color-gray-30; - height: 20px; - width: 20px; - - &:hover { - fill: $color-gray-20; - } - + height: 100%; + width: 100%; } - - span.delete { - - &:hover { - - svg{ - fill: $color-danger-light; - } - - } - - } - - } - -} - -.move-item { - position: relative; - - .move-list { - background-color: $color-gray-10; - border-radius: $br-small; - bottom: 30px; - display: flex; - flex-direction: column; - left: -30px; - max-height: 260px; - overflow-y: scroll; - padding: $medium; - position: absolute; - width: 260px; - - li { - padding-bottom: $medium; - - &.title { - color: $color-gray-50; - } - - } - } } -.grid-files-empty { - align-items: center; - border: 1px dashed $color-gray-20; - border-radius: $br-small; - display: flex; - flex-direction: column; - height: fit-content; - margin: $size-4; - padding: 3rem; +.grid-empty-placeholder { + align-items: center; + border: 1px dashed $color-gray-20; + border-radius: $br-small; + display: flex; + flex-direction: column; + height: 200px; + margin: $size-4; + padding: 3rem; + justify-content: center; - .grid-files-desc { - color: $color-gray-60; - margin-bottom: $medium; - } + svg { + width: 36px; + height: 36px; + fill: $color-gray-20; + } + + .text { + margin-top: 10px; + color: $color-gray-30; + font-size: $fs16; + } } + diff --git a/frontend/resources/styles/main/partials/dashboard-header.scss b/frontend/resources/styles/main/partials/dashboard-header.scss new file mode 100644 index 000000000..43648c8e7 --- /dev/null +++ b/frontend/resources/styles/main/partials/dashboard-header.scss @@ -0,0 +1,100 @@ +// 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/. +// +// This Source Code Form is "Incompatible With Secondary Licenses", as +// defined by the Mozilla Public License, v. 2.0. +// +// Copyright (c) 2020 UXBOX Labs SL + +.dashboard-header { + align-items: center; + background-color: $color-white; + display: flex; + height: 63px; + padding: $x-small $small; + position: relative; + z-index: 10; + + .element-name { + margin-right: $small; + } + + .btn-secondary { + flex-shrink: 0; + margin-left: auto; + z-index: 10; + height: 32px; + } + + svg { + fill: $color-black; + height: 14px; + margin-right: $x-small; + width: 14px; + } + + nav { + ul { + align-items: center; + bottom: 0; + display: flex; + font-size: $fs15; + justify-content: center; + margin: auto; + position: absolute; + width: 100%; + z-index: 1; + } + + li { + a { + align-items: center; + border-bottom: 3px solid transparent; + color: $color-gray-30; + display: flex; + height: 40px; + padding: $x-small $big; + flex-basis: 140px; + + &:hover { + color: $color-black; + } + + } + + &.current { + a { + color: $color-black; + border-color: $color-primary; + } + } + } + } + + .dashboard-title { + color: $color-black; + display: flex; + flex-shrink: 0; + font-size: $fs18; + z-index: 10; + } + + .icon { + display: flex; + align-items: center; + cursor: pointer; + margin-left: $small; + z-index: 10; + + svg { + fill: $color-gray-40; + width: 15px; + height: 15px; + + &:hover { + fill: $color-primary-dark; + } + } + } +} diff --git a/frontend/resources/styles/main/partials/dashboard-sidebar.scss b/frontend/resources/styles/main/partials/dashboard-sidebar.scss index 49e86f416..5c5efa5ce 100644 --- a/frontend/resources/styles/main/partials/dashboard-sidebar.scss +++ b/frontend/resources/styles/main/partials/dashboard-sidebar.scss @@ -2,104 +2,335 @@ // 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) 2015-2016 Andrey Antukh -// Copyright (c) 2015-2016 Juan de la Cruz +// This Source Code Form is "Incompatible With Secondary Licenses", as +// defined by the Mozilla Public License, v. 2.0. +// +// Copyright (c) 2020 UXBOX Labs SL .dashboard-sidebar { background-color: $color-white; - .sidebar-team { + .sidebar-inside { display: flex; flex-direction: column; - padding: $size-4 0; - border-top: 1px solid $color-gray-10; height: 100%; - padding-bottom: 2.8rem; + padding-top: $size-2; } - .dashboard-sidebar-inside { + .sidebar-content { display: flex; flex-direction: column; height: 100%; - border-right: 1px solid $color-gray-10; + overflow-y: auto; + padding: 0; - .dashboard-elements { - display: flex; - flex-direction: column; - overflow-y: auto; + hr { + margin: 10px 15px; + border-color: $color-gray-10; + } + } + + .dropdown { + position: absolute; + max-height: 30rem; + overflow-y: auto; + background-color: $color-white; + border-radius: 4px; + box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.25); + + hr { margin: 0; + border-color: $color-gray-10; + } - &.dashboard-common { - overflow: unset; + li { + color: $color-gray-60; + cursor: pointer; + font-size: $fs14; + display: flex; + padding: 13px 16px; + + &.title { + font-weight: 600; + cursor: default; } - li { - align-items: center; - cursor: pointer; + &.team-item { display: flex; - flex-shrink: 0; - padding: $size-2; - svg { - border-radius: 3px; - fill: $color-black; - margin-right: 8px; - height: $size-3; - width: $size-3; - } - - span.element-title { - color: $color-gray-60; - font-size: $fs14; - overflow-x: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - - &.recent-projects { + .icon { + display: flex; + align-items: center; + padding-right: 10px; svg { - fill: $color-white; + width: 20px; + height: 20px; + fill: $color-gray-60; } } + } - & .edit-wrapper { - display: flex; - } + &:hover { + background-color: $color-primary-lighter; + } + } + } - input.element-title { - border: 0; - height: 30px; - padding: 5px; - margin: 0; - width: 100%; - background-color: $color-white; - } + .sidebar-team-switch { + position: relative; + display: flex; + margin: 5px 15px; - .close { - background-color: $color-white; - cursor: pointer; - padding: 3px 5px; + .teams-dropdown { + left: 0; + top: 50px; + z-index: 12; + max-height: 30rem; + min-width: 189px; + } - svg { - fill: $color-gray-30; - height: 15px; - transform: rotate(45deg) translateY(7px); - width: 15px; - margin: 0; - } - } + .options-dropdown { + left: 80px; + top: 50px; + z-index: 12; + max-height: 30rem; + min-width: 110px; + } - .element-subtitle { - color: $color-gray-20; - font-style: italic; - } + .switch-content { + height: 40px; + display: flex; + width: 100%; + border: 1px solid $color-gray-10; + border-radius: 6px; + align-items: center; + } - &:hover, - &.current { - background-color: $color-primary-lighter; + .switch-options { + display: flex; + max-width: 22px; + min-width: 22px; + border-left: 1px solid $color-gray-10; + justify-content: center; + align-items: center; + cursor: pointer; + + svg { + width: 15px; + height: 13px; + fill: $color-gray-60; + } + } + + .current-team { + padding: 0px 10px; + display: flex; + flex-grow: 1; + + .team-name { + flex-grow: 1; + display: flex; + align-items: center; + + .team-text { color: $color-gray-60; } } + + } + + .team-icon { + display: flex; + align-items: center; + padding-right: 10px; + + svg { + width: 23px; + height: 23px; + fill: $color-gray-60; + } + } + + .switch-icon { + display: flex; + align-items: center; + cursor: pointer; + + svg { + width: 10px; + height: 10px; + fill: $color-gray-60; + } + } + } + + .sidebar-empty-placeholder { + padding: 10px 12px; + color: $color-gray-30; + display: flex; + align-items: flex-start; + + .icon { + padding: 0px 10px; + svg { + fill: $color-gray-30; + width: 12px; + height: 12px; + } + } + .text { + font-size: $fs13; + } + } + + .sidebar-nav { + display: flex; + flex-direction: column; + overflow-y: auto; + margin: 0; + + // TODO: should be deprecated / unclear name + &.dashboard-common { + overflow: unset; + } + + &.no-overflow { + overflow: unset; + } + + li { + align-items: center; + cursor: pointer; + display: flex; + flex-shrink: 0; + padding: $size-2; + + svg { + border-radius: 3px; + fill: $color-black; + margin-right: 8px; + height: $size-3; + width: $size-3; + } + + span.element-title { + color: $color-gray-60; + font-size: $fs14; + overflow-x: hidden; + text-overflow: ellipsis; + white-space: nowrap; + user-select: none; + } + + &::before { + background-color: transparent; + border-radius: $br-small; + content: ""; + height: 26px; + margin-right: 6px; + width: 4px; + } + + &.recent-projects { + svg { + fill: $color-white; + } + } + + & .edit-wrapper { + border: 1px solid $color-gray-10; + border-radius: $br-small; + display: flex; + width: 100%; + } + + input.element-title { + border: 0; + height: 30px; + padding: 5px; + margin: 0; + width: 100%; + background-color: $color-white; + } + + .close { + background-color: $color-white; + cursor: pointer; + padding: 3px 5px; + + svg { + fill: $color-gray-30; + height: 15px; + transform: rotate(45deg) translateY(7px); + width: 15px; + margin: 0; + } + } + + .element-subtitle { + color: $color-gray-20; + font-style: italic; + } + + &:hover { + &::before { + background-color: $color-gray-10; + } + } + + &.current { + font-weight: bold; + + &::before { + background-color: $color-primary; + } + } + } + } + + .sidebar-search { + align-items: center; + border: 1px solid $color-gray-10; + border-radius: 4px; + display: flex; + margin: 5px 15px; + + .input-text { + background: $color-white; + border: 0; + color: $color-gray-60; + font-size: $fs14; + padding: 6px; + margin: 0; + max-width: 170px; + width: 100%; + height: 40px; + } + + &:focus, + &:focus-within { + border-color: $color-black; + } + + .clear-search { + align-items: center; + cursor: pointer; + display: flex; + height: 22px; + margin-left: auto; + padding: 0 $small; + width: 32px; + + svg { + fill: $color-gray-30; + height: 15px; + transform: rotate(45deg); + width: 15px; + + &:hover { + fill: $color-danger; + } + } } } @@ -117,6 +348,7 @@ display: flex; margin-top: 1rem; padding: $size-2; + position: relative; span { color: $color-gray-30; @@ -126,50 +358,87 @@ .btn-icon-light { margin-left: auto; } + + &::before { + background-color: $color-gray-10; + content: ""; + height: 1px; + left: 4%; + position: absolute; + right: 4%; + top: -5px; + width: 92%; + } } -.dashboard-search { - align-items: center; - border: 1px solid $color-gray-10; - display: flex; - margin: $size-2; - - .input-text { - background: $color-white; - border: 0; - color: $color-gray-60; - font-size: $fs14; - padding: 4px 8px; - margin: 0; - max-width: 170px; - width: 100%; +.team-form-modal { + h2 { + font-weight: 400; + color: $color-gray-40; + font-size: 28px; + margin-bottom: 30px; } - &:focus, - &:focus-within { - border-color: $color-black; - } - - .clear-search { - align-items: center; - cursor: pointer; + .buttons-row { + margin-top: 20px; display: flex; - height: 22px; - padding: 0 5px; - width: 22px; - - svg { - fill: $color-gray-30; - height: 15px; - transform: rotate(45deg); - width: 15px; - - &:hover { - fill: $color-danger; - } - - } - + justify-content: center; } + input[type=submit] { + width: 120px; + margin-bottom: 0px; + } +} + + +.profile-section { + align-items: center; + cursor: pointer; + display: flex; + padding: $small; + position: relative; + + span { + @include text-ellipsis; + color: $color-black; + margin: $small; + font-size: $fs12; + max-width: 135px; + } + + img { + border-radius: 50%; + flex-shrink: 0; + height: 25px; + width: 25px; + } + + .dropdown { + left: 15px; + bottom: 50px; + z-index: 12; + max-height: 30rem; + min-width: 189px; + + position: absolute; + bottom: 45px; + z-index: 12; + width: 170px; + + @include animation(0,.2s,fadeInUp); + + li { + display: flex; + align-items: center; + font-size: $fs13; + padding: 5px 10px; + + svg { + fill: $color-gray-20; + height: 12px; + width: 12px; + } + } + } } diff --git a/frontend/resources/styles/main/partials/dashboard.scss b/frontend/resources/styles/main/partials/dashboard.scss new file mode 100644 index 000000000..1fb8d8617 --- /dev/null +++ b/frontend/resources/styles/main/partials/dashboard.scss @@ -0,0 +1,76 @@ +// 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/. +// +// This Source Code Form is "Incompatible With Secondary Licenses", as +// defined by the Mozilla Public License, v. 2.0. +// +// Copyright (c) 2020 UXBOX Labs SL + +.dashboard-grid-container { + background-color: $color-dashboard; + border-top-right-radius: $br-huge; + border-top-left-radius: $br-huge; + flex: 1 0 0; + margin-right: $small; + overflow-y: auto; + + + &.search { + margin-top: 10px; + } +} + +.dashboard-project-row { + margin-bottom: $medium; + + .project { + align-items: center; + background: $color-white; + border-radius: $br-small; + display: flex; + flex-direction: row; + margin-left: $medium; + margin-top: $medium; + padding: $x-small $x-small $x-small $small; + width: fit-content; + height: 40px; + + .btn-secondary { + margin-left: $big; + height: 32px; + } + + h2 { + cursor: pointer; + font-size: 15px; + line-height: 1rem; + font-weight: unset; + color: $color-black; + margin-right: $medium; + } + + .info { + font-size: 15px; + line-height: 1rem; + font-weight: unset; + color: $color-gray-30; + } + + .pin-icon { + cursor: pointer; + display: flex; + align-items: center; + margin-right: 10px; + svg { + width: 15px; + height: 15px; + fill: $color-gray-20; + } + + &.active { + svg { fill: $color-gray-50; } + } + } + } +} diff --git a/frontend/resources/styles/main/partials/login.scss b/frontend/resources/styles/main/partials/login.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/frontend/resources/styles/main/partials/main-bar.scss b/frontend/resources/styles/main/partials/main-bar.scss deleted file mode 100644 index 3ae31ac5b..000000000 --- a/frontend/resources/styles/main/partials/main-bar.scss +++ /dev/null @@ -1,167 +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) 2015-2016 Andrey Antukh -// Copyright (c) 2015-2016 Juan de la Cruz - -.main-bar { - align-items: center; - background-color: $color-white; - border-bottom: 1px solid $color-gray-10; - display: flex; - height: 40px; - padding: $x-small $small; - padding-left: $x-big; - position: relative; - z-index: 10; - - .element-name { - margin-right: $small; - } - - .btn-secondary { - flex-shrink: 0; - margin-left: auto; - } - - svg { - fill: $color-black; - height: 14px; - margin-right: $x-small; - width: 14px; - } - -} - -.main-logo { - border-right: 1px solid $color-gray-10; - border-bottom: 1px solid $color-gray-10; - text-align: center; - padding-top: $x-small; - - svg { - fill: $color-black; - height: 30px; - width: 30px; - } -} - -.main-nav { - align-items: center; - display: flex; - font-size: $fs15; - height: 35px; - margin: 0 0 0 120px; - - li { - - a { - border-bottom: 2px solid transparent; - color: $color-gray-10; - margin: $x-small $big; - - &:hover { - border-color: $color-primary; - } - - } - - &.current { - - a { - border-color: $color-primary; - } - - } - - } - -} - -.dashboard-title { - color: $color-black; - display: flex; - font-size: $fs15; -} - -.main-bar-icon { - cursor: pointer; - margin-left: $small; - - svg { - fill: $color-gray-40; - width: 10px; - - &:hover { - fill: $color-primary-dark; - } - } -} - -.user-zone { - align-items: center; - border-right: 1px solid $color-gray-10; - border-bottom: 1px solid $color-gray-10; - cursor: pointer; - display: flex; - padding: 0 $x-small 0 $small; - position: relative; - width: 180px; - - span { - @include text-ellipsis; - color: $color-black; - margin: $small; - font-size: $fs12; - } - - img { - border-radius: 50%; - flex-shrink: 0; - height: 25px; - width: 25px; - } - - ul.profile-menu { - position: absolute; - top: 0; - left: 0; - z-index: 12; - width: 180px; - background-color: $color-gray-60; - border-radius: $br-small; - padding: 0 $small; - - @include animation(0,.2s,fadeInDown); - - li { - font-size: $fs13; - padding: $small 0; - - svg { - fill: $color-gray-20; - height: 12px; - width: 12px; - } - - span { - color: $color-white; - } - - &:hover { - - span { - color: $color-primary; - } - - svg { - fill: $color-primary; - } - - } - - } - } - -} diff --git a/frontend/resources/styles/main/partials/modal.scss b/frontend/resources/styles/main/partials/modal.scss index ddde64301..45ef29d24 100644 --- a/frontend/resources/styles/main/partials/modal.scss +++ b/frontend/resources/styles/main/partials/modal.scss @@ -40,7 +40,7 @@ display: flex; flex-grow: 1; flex-direction: column; - padding: 100px; + padding: 60px 100px; } .button-row { diff --git a/frontend/src/app/main.cljs b/frontend/src/app/main.cljs index 1a4621504..a3a094d3d 100644 --- a/frontend/src/app/main.cljs +++ b/frontend/src/app/main.cljs @@ -43,7 +43,6 @@ profile (:profile storage) authed? (and (not (nil? profile)) (not= (:id profile) uuid/zero))] - (cond (and (or (= path "") (nil? match)) @@ -51,7 +50,7 @@ (st/emit! (rt/nav :auth-login)) (and (nil? match) authed?) - (st/emit! (rt/nav :dashboard-team {:team-id (:default-team-id profile)})) + (st/emit! (rt/nav :dashboard-projects {:team-id (:default-team-id profile)})) (nil? match) (st/emit! (rt/nav :not-found)) diff --git a/frontend/src/app/main/data/dashboard.cljs b/frontend/src/app/main/data/dashboard.cljs index edf22fa88..81fdbf19e 100644 --- a/frontend/src/app/main/data/dashboard.cljs +++ b/frontend/src/app/main/data/dashboard.cljs @@ -6,18 +6,18 @@ (ns app.main.data.dashboard (:require - [beicon.core :as rx] - [cljs.spec.alpha :as s] - [cuerdas.core :as str] - [potok.core :as ptk] [app.common.data :as d] [app.common.pages :as cp] [app.common.spec :as us] + [app.common.uuid :as uuid] [app.main.repo :as rp] [app.util.router :as rt] [app.util.time :as dt] [app.util.timers :as ts] - [app.common.uuid :as uuid])) + [beicon.core :as rx] + [cljs.spec.alpha :as s] + [cuerdas.core :as str] + [potok.core :as ptk])) ;; --- Specs @@ -50,188 +50,96 @@ ::modified-at ::project-id])) -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Initialization -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -(declare search-files) - -(defn initialize-search - [team-id search-term] - (ptk/reify ::initialize-search - ptk/UpdateEvent - (update [_ state] - (update state :dashboard-local assoc - :search-result nil)) - - ptk/WatchEvent - (watch [_ state stream] - (let [local (:dashboard-local state)] - (when-not (empty? search-term) - (rx/of (search-files team-id search-term))))))) - - -(declare fetch-files) -(declare fetch-projects) -(declare fetch-recent-files) -(declare fetch-shared-files) - -(def initialize-drafts - (ptk/reify ::initialize-drafts - ptk/UpdateEvent - (update [_ state] - (let [profile (:profile state)] - (update state :dashboard-local assoc - :project-for-edit nil - :team-id (:default-team-id profile) - :project-id (:default-project-id profile)))) - - ptk/WatchEvent - (watch [_ state stream] - (let [local (:dashboard-local state)] - (rx/of (fetch-files (:project-id local)) - (fetch-projects (:team-id local) nil)))))) - - -(defn initialize-recent - [team-id] - (us/verify ::us/uuid team-id) - (ptk/reify ::initialize-recent - ptk/UpdateEvent - (update [_ state] - (update state :dashboard-local assoc - :project-for-edit nil - :project-id nil - :team-id team-id)) - - ptk/WatchEvent - (watch [_ state stream] - (let [local (:dashboard-local state)] - (rx/of (fetch-projects (:team-id local) nil) - (fetch-recent-files (:team-id local))))))) - - -(defn initialize-project - [team-id project-id] - (us/verify ::us/uuid team-id) - (us/verify ::us/uuid project-id) - (ptk/reify ::initialize-project - ptk/UpdateEvent - (update [_ state] - (update state :dashboard-local assoc - :project-for-edit nil - :team-id team-id - :project-id project-id)) - - ptk/WatchEvent - (watch [_ state stream] - (let [local (:dashboard-local state)] - (rx/of (fetch-projects (:team-id local) nil) - (fetch-files (:project-id local))))))) - - -(defn initialize-libraries - [team-id] - (us/verify ::us/uuid team-id) - (ptk/reify ::initialize-libraries - ptk/UpdateEvent - (update [_ state] - (update state :dashboard-local assoc - :project-for-edit nil - :project-id nil - :team-id team-id)) - - ptk/WatchEvent - (watch [_ state stream] - (let [local (:dashboard-local state)] - (rx/of (fetch-projects (:team-id local) nil) - (fetch-shared-files (:team-id local))))))) - ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Data Fetching ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; --- Fetch Team + +(defn fetch-team + [{:keys [id] :as params}] + (letfn [(fetched [team state] + (update state :teams assoc id team))] + (ptk/reify ::fetch-team + ptk/WatchEvent + (watch [_ state stream] + (->> (rp/query :team params) + (rx/map #(partial fetched %))))))) + + ;; --- Fetch Projects -(declare projects-fetched) - (defn fetch-projects - [team-id project-id] + [{:keys [team-id] :as params}] (us/assert ::us/uuid team-id) - (us/assert (s/nilable ::us/uuid) project-id) - (ptk/reify ::fetch-projects - ptk/WatchEvent - (watch [_ state stream] - (->> (rp/query :projects {:team-id team-id}) - (rx/map projects-fetched) - #_(rx/catch (fn [error] - (rx/of (rt/nav' :auth-login)))))))) + (letfn [(fetched [projects state] + (assoc-in state [:projects team-id] (d/index-by :id projects)))] + (ptk/reify ::fetch-projects + ptk/WatchEvent + (watch [_ state stream] + (->> (rp/query :projects {:team-id team-id}) + (rx/map #(partial fetched %))))))) -(defn projects-fetched - [projects] - (us/verify (s/every ::project) projects) - (ptk/reify ::projects-fetched - ptk/UpdateEvent - (update [_ state] - (assoc state :projects (d/index-by :id projects))))) ;; --- Search Files -(declare files-searched) +(s/def :internal.event.search-files/team-id ::us/uuid) +(s/def :internal.event.search-files/search-term (s/nilable ::us/string)) + +(s/def :internal.event/search-files + (s/keys :req-un [:internal.event.search-files/search-term + :internal.event.search-files/team-id])) (defn search-files - [team-id search-term] - (us/assert ::us/uuid team-id) - (us/assert ::us/string search-term) - (ptk/reify ::search-files - ptk/WatchEvent - (watch [_ state stream] - (->> (rp/query :search-files {:team-id team-id :search-term search-term}) - (rx/map files-searched))))) + [params] + (us/assert :internal.event/search-files params) + (letfn [(fetched [result state] + (update state :dashboard-local + assoc :search-result result))] + (ptk/reify ::search-files + ptk/UpdateEvent + (update [_ state] + (update state :dashboard-local + assoc :search-result nil)) -(defn files-searched - [files] - (us/verify (s/every ::file) files) - (ptk/reify ::files-searched - ptk/UpdateEvent - (update [_ state] - (update state :dashboard-local assoc - :search-result files)))) + ptk/WatchEvent + (watch [_ state stream] + (->> (rp/query :search-files params) + (rx/map #(partial fetched %))))))) ;; --- Fetch Files (defn fetch-files - [project-id] + [{:keys [project-id] :as params}] (us/assert ::us/uuid project-id) - (letfn [(on-fetched [files state] - (assoc state :files (d/index-by :id files)))] + (letfn [(fetched [files state] + (update state :files assoc project-id (d/index-by :id files)))] (ptk/reify ::fetch-files ptk/WatchEvent (watch [_ state stream] - (let [params {:project-id project-id}] - (->> (rp/query :files params) - (rx/map #(partial on-fetched %)))))))) + (->> (rp/query :files params) + (rx/map #(partial fetched %))))))) ;; --- Fetch Shared Files (defn fetch-shared-files - [team-id] - (letfn [(on-fetched [files state] - (let [files (d/index-by :id files)] - (assoc state :files files)))] + [{:keys [team-id] :as params}] + (us/assert ::us/uuid team-id) + (letfn [(fetched [files state] + (update state :shared-files assoc team-id (d/index-by :id files)))] (ptk/reify ::fetch-shared-files ptk/WatchEvent (watch [_ state stream] (->> (rp/query :shared-files {:team-id team-id}) - (rx/map #(partial on-fetched %))))))) + (rx/map #(partial fetched %))))))) ;; --- Fetch recent files (declare recent-files-fetched) (defn fetch-recent-files - [team-id] + [{:keys [team-id] :as params}] (us/assert ::us/uuid team-id) (ptk/reify ::fetch-recent-files ptk/WatchEvent @@ -241,21 +149,16 @@ (rx/map recent-files-fetched)))))) (defn recent-files-fetched - [recent-files] + [files] (ptk/reify ::recent-files-fetched ptk/UpdateEvent (update [_ state] - (let [flatten-files #(reduce (fn [acc [project-id files]] - (merge acc (d/index-by :id files))) - {} - %1) - extract-ids #(reduce (fn [acc [project-id files]] - (assoc acc project-id (map :id files))) - {} - %1)] - (assoc state - :files (flatten-files recent-files) - :recent-file-ids (extract-ids recent-files)))))) + (reduce-kv (fn [state project-id files] + (-> state + (update-in [:files project-id] merge (d/index-by :id files)) + (assoc-in [:recent-files project-id] (into #{} (map :id) files)))) + state + (group-by :project-id files))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Data Modification @@ -263,30 +166,37 @@ ;; --- Create Project -(declare project-created) - -(def create-project - (ptk/reify ::create-project +(defn create-team + [{:keys [name] :as params}] + (us/assert string? name) + (ptk/reify ::create-team ptk/WatchEvent (watch [_ state stream] - (let [name (name (gensym "New Project ")) - team-id (get-in state [:dashboard-local :team-id])] - (->> (rp/mutation! :create-project {:name name :team-id team-id}) - (rx/map project-created)))))) + (let [{:keys [on-success on-error] + :or {on-success identity + on-error identity}} (meta params)] + (->> (rp/mutation! :create-team {:name name}) + (rx/tap on-success) + (rx/catch on-error)))))) -(defn project-created - [data] - (us/verify ::project data) - (ptk/reify ::project-created - ptk/UpdateEvent - (update [_ state] - (-> state - (update :projects assoc (:id data) data) - (update :dashboard-local assoc :project-for-edit (:id data)))) - - ptk/WatchEvent - (watch [_ state stream] - (rx/of (rt/nav :dashboard-project {:team-id (:team-id data) :project-id (:id data)}))))) +(defn create-project + [{:keys [team-id] :as params}] + (us/assert ::us/uuid team-id) + (letfn [(created [project state] + (-> state + (assoc-in [:projects team-id (:id project)] project) + (assoc-in [:dashboard-local :project-for-edit] (:id project))))] + (ptk/reify ::create-project + ptk/WatchEvent + (watch [_ state stream] + (let [name (name (gensym "New Project ")) + {:keys [on-success on-error] + :or {on-success identity + on-error identity}} (meta params)] + (->> (rp/mutation! :create-project {:name name :team-id team-id}) + (rx/tap on-success) + (rx/map #(partial created %)) + (rx/catch on-error))))))) (def clear-project-for-edit (ptk/reify ::clear-project-for-edit @@ -294,15 +204,29 @@ (update [_ state] (assoc-in state [:dashboard-local :project-for-edit] nil)))) +(defn toggle-project-pin + [{:keys [id is-pinned team-id] :as params}] + (us/assert ::project params) + (ptk/reify ::toggle-project-pin + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:projects team-id id :is-pinned] (not is-pinned))) + + ptk/WatchEvent + (watch [_ state stream] + (let [params (select-keys params [:id :is-pinned :team-id])] + (->> (rp/mutation :toggle-project-pin params) + (rx/ignore)))))) + ;; --- Rename Project (defn rename-project - [id name] - {:pre [(uuid? id) (string? name)]} + [{:keys [id name team-id] :as params}] + (us/assert ::project params) (ptk/reify ::rename-project ptk/UpdateEvent (update [_ state] - (assoc-in state [:projects id :name] name)) + (assoc-in state [:projects team-id id :name] name)) ptk/WatchEvent (watch [_ state stream] @@ -313,12 +237,12 @@ ;; --- Delete Project (by id) (defn delete-project - [id] - (us/verify ::us/uuid id) + [{:keys [id team-id] :as params}] + (us/assert ::project params) (ptk/reify ::delete-project ptk/UpdateEvent (update [_ state] - (update state :projects dissoc id)) + (update-in state [:projects team-id] dissoc id)) ptk/WatchEvent (watch [_ state s] @@ -328,16 +252,14 @@ ;; --- Delete File (by id) (defn delete-file - [id] - (us/verify ::us/uuid id) + [{:keys [id project-id] :as params}] + (us/assert ::file params) (ptk/reify ::delete-file ptk/UpdateEvent (update [_ state] - (let [project-id (get-in state [:files id :project-id]) - recent-project-files (get-in state [:recent-file-ids project-id] [])] - (-> state - (update :files dissoc id) - (assoc-in [:recent-file-ids project-id] (remove #(= % id) recent-project-files))))) + (-> state + (update-in [:files project-id] dissoc id) + (update-in [:recent-files project-id] (fnil disj #{}) id))) ptk/WatchEvent (watch [_ state s] @@ -347,16 +269,16 @@ ;; --- Rename File (defn rename-file - [id name] - {:pre [(uuid? id) (string? name)]} + [{:keys [id name project-id] :as params}] + (us/assert ::file params) (ptk/reify ::rename-file ptk/UpdateEvent (update [_ state] - (assoc-in state [:files id :name] name)) + (assoc-in state [:files project-id id :name] name)) ptk/WatchEvent (watch [_ state stream] - (let [params {:id id :name name}] + (let [params (select-keys params [:id :name])] (->> (rp/mutation :rename-file params) (rx/ignore)))))) @@ -381,48 +303,29 @@ (declare file-created) (defn create-file - [project-id] + [{:keys [project-id] :as params}] + (us/assert ::us/uuid project-id) (ptk/reify ::create-file ptk/WatchEvent (watch [_ state stream] - (let [name (name (gensym "New File ")) - params {:name name :project-id project-id}] + (let [{:keys [on-success on-error] + :or {on-success identity + on-error identity}} (meta params) + + name (name (gensym "New File ")) + params (assoc params :name name)] + (->> (rp/mutation! :create-file params) - (rx/map file-created)))))) + (rx/tap on-success) + (rx/map file-created) + (rx/catch on-error)))))) (defn file-created - [data] - (us/verify ::file data) + [{:keys [project-id id] :as file}] + (us/verify ::file file) (ptk/reify ::file-created ptk/UpdateEvent (update [_ state] - (let [project-id (:project-id data) - file-id (:id data) - recent-project-files (get-in state [:recent-file-ids project-id] [])] - (-> state - (assoc-in [:files file-id] data) - (assoc-in [:recent-file-ids project-id] (conj recent-project-files file-id))))) - - ptk/WatchEvent - (watch [_ state stream] - (let [pparams {:project-id (:project-id data) - :file-id (:id data)} - qparams {:page-id (get-in data [:data :pages 0])}] - (rx/of (rt/nav :workspace pparams qparams)))))) - - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; UI State Handling -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -;; --- Update Opts (Filtering & Ordering) - -;; (defn update-opts -;; [& {:keys [order filter] :as opts}] -;; (ptk/reify ::update-opts -;; ptk/UpdateEvent -;; (update [_ state] -;; (update state :dashboard-local merge -;; (when order {:order order}) -;; (when filter {:filter filter}))))) - + (-> state + (assoc-in [:files project-id id] file) + (update-in [:recent-files project-id] (fnil conj #{}) id))))) diff --git a/frontend/src/app/main/data/workspace/persistence.cljs b/frontend/src/app/main/data/workspace/persistence.cljs index 7d08815f1..3d99ec98f 100644 --- a/frontend/src/app/main/data/workspace/persistence.cljs +++ b/frontend/src/app/main/data/workspace/persistence.cljs @@ -208,7 +208,7 @@ (watch [_ state stream] (->> (rx/zip (rp/query :file {:id file-id}) (rp/query :file-users {:id file-id}) - (rp/query :project-by-id {:project-id project-id}) + (rp/query :project {:id project-id}) (rp/query :file-libraries {:file-id file-id})) (rx/first) (rx/map (fn [bundle] (apply bundle-fetched bundle))) diff --git a/frontend/src/app/main/store.cljs b/frontend/src/app/main/store.cljs index cf645ca83..b2ac7f363 100644 --- a/frontend/src/app/main/store.cljs +++ b/frontend/src/app/main/store.cljs @@ -51,6 +51,10 @@ (apply ptk/emit! store (cons event events)) nil)) +(defn emitf + [& events] + #(apply ptk/emit! store events)) + (def initial-state {:session-id (uuid/next) :profile (:profile storage)}) diff --git a/frontend/src/app/main/ui.cljs b/frontend/src/app/main/ui.cljs index c982d023a..ff002f454 100644 --- a/frontend/src/app/main/ui.cljs +++ b/frontend/src/app/main/ui.cljs @@ -62,10 +62,10 @@ ["/dashboard" ["/team/:team-id" - ["/" :dashboard-team] + ["/projects" :dashboard-projects] ["/search" :dashboard-search] - ["/project/:project-id" :dashboard-project] - ["/libraries" :dashboard-libraries]]] + ["/libraries" :dashboard-libraries] + ["/projects/:project-id" :dashboard-files]]] ["/workspace/:project-id/:file-id" :workspace]]) @@ -74,7 +74,9 @@ (let [data (ex-data error)] (case (:type data) :not-found [:& not-found-page {:error data}] - [:span "Internal application errror"]))) + (do + (ptk/handle-error error) + [:span "Internal application errror"])))) (mf/defc app {::mf/wrap [#(mf/catch % {:fallback app-error})]} @@ -105,8 +107,8 @@ ]) (:dashboard-search - :dashboard-team - :dashboard-project + :dashboard-projects + :dashboard-files :dashboard-libraries) [:& dashboard {:route route}] diff --git a/frontend/src/app/main/ui/dashboard.cljs b/frontend/src/app/main/ui/dashboard.cljs index e0c7ce10e..85b7f2179 100644 --- a/frontend/src/app/main/ui/dashboard.cljs +++ b/frontend/src/app/main/ui/dashboard.cljs @@ -9,22 +9,23 @@ (ns app.main.ui.dashboard (:require - [cuerdas.core :as str] - [rumext.alpha :as mf] - [app.main.ui.icons :as i] [app.common.exceptions :as ex] - [app.common.uuid :as uuid] [app.common.spec :as us] - [app.main.store :as st] + [app.common.uuid :as uuid] + [app.main.data.dashboard :as dd] [app.main.refs :as refs] - [app.main.ui.dashboard.sidebar :refer [sidebar]] - [app.main.ui.dashboard.search :refer [search-page]] - [app.main.ui.dashboard.project :refer [project-page]] - [app.main.ui.dashboard.recent-files :refer [recent-files-page]] + [app.main.store :as st] + [app.main.ui.dashboard.files :refer [files-section]] [app.main.ui.dashboard.libraries :refer [libraries-page]] - [app.main.ui.dashboard.profile :refer [profile-section]] + [app.main.ui.dashboard.projects :refer [projects-section]] + [app.main.ui.dashboard.search :refer [search-page]] + [app.main.ui.dashboard.sidebar :refer [sidebar]] + [app.main.ui.icons :as i] + [app.util.i18n :as i18n :refer [t]] [app.util.router :as rt] - [app.util.i18n :as i18n :refer [t]])) + [cuerdas.core :as str] + [okulary.core :as l] + [rumext.alpha :as mf])) (defn ^boolean uuid-str? [s] @@ -44,40 +45,71 @@ (assoc :team-id (uuid team-id)) (uuid-str? project-id) - (assoc :project-id (uuid project-id)) + (assoc :project-id (uuid project-id))))) - ;; TODO: delete the usage of "drafts" +(defn- team-ref + [id] + (l/derived (l/in [:teams id]) st/state)) - (= "drafts" project-id) - (assoc :project-id (:default-project-id profile))))) +(defn- projects-ref + [team-id] + (l/derived (l/in [:projects team-id]) st/state)) + +(mf/defc dashboard-content + [{:keys [team projects project section search-term] :as props}] + [:div.dashboard-content + (case section + :dashboard-projects + [:& projects-section {:team team + :projects projects}] + + :dashboard-files + (when project + [:& files-section {:team team :project project}]) + + + :dashboard-search + [:& search-page {:team team + :search-term search-term}] + + :dashboard-libraries + [:& libraries-page {:team team}] + + nil)]) (mf/defc dashboard [{:keys [route] :as props}] - (let [profile (mf/deref refs/profile) - page (get-in route [:data :name]) - {:keys [search-term team-id project-id] :as params} (parse-params route profile)] + (let [profile (mf/deref refs/profile) + section (get-in route [:data :name]) + params (parse-params route profile) + + project-id (:project-id params) + team-id (:team-id params) + search-term (:search-term params) + + projects-ref (mf/use-memo (mf/deps team-id) #(projects-ref team-id)) + team-ref (mf/use-memo (mf/deps team-id) #(team-ref team-id)) + + team (mf/deref team-ref) + projects (mf/deref projects-ref) + project (get projects project-id)] + + (mf/use-effect + (mf/deps team-id) + (fn [] + (st/emit! (dd/fetch-team {:id team-id}) + (dd/fetch-projects {:team-id team-id})))) + [:section.dashboard-layout - [:div.main-logo - [:a {:on-click #(st/emit! (rt/nav :dashboard-team {:team-id team-id}))} - i/logo-icon]] - [:& profile-section {:profile profile}] - [:& sidebar {:team-id team-id - :project-id project-id - :section page + [:& sidebar {:team team + :projects projects + :project project + :section section :search-term search-term}] - [:div.dashboard-content - (case page - :dashboard-search - [:& search-page {:team-id team-id :search-term search-term}] - - :dashboard-team - [:& recent-files-page {:team-id team-id}] - - :dashboard-libraries - [:& libraries-page {:team-id team-id}] - - :dashboard-project - [:& project-page {:team-id team-id - :project-id project-id}])]])) - + (when team + [:& dashboard-content {:projects projects + :project project + :section section + :search-term search-term + :team team}])])) diff --git a/frontend/src/app/main/ui/dashboard/common.cljs b/frontend/src/app/main/ui/dashboard/common.cljs deleted file mode 100644 index 0acbd96be..000000000 --- a/frontend/src/app/main/ui/dashboard/common.cljs +++ /dev/null @@ -1,58 +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) 2019 Andrey Antukh - -(ns app.main.ui.dashboard.common - (:require - [cuerdas.core :as str] - [rumext.alpha :as mf] - [app.main.ui.icons :as i] - [app.main.ui.keyboard :as k] - [app.util.dom :as dom] - [app.util.i18n :as t :refer [tr]])) - -;; --- Page Title - -(mf/defc grid-header - [{:keys [on-change on-delete value read-only?] :as props}] - (let [edit? (mf/use-state false) - input (mf/use-ref nil)] - (letfn [(save [] - (let [new-value (-> (mf/ref-val input) - (dom/get-inner-text) - (str/trim))] - (on-change new-value) - (reset! edit? false))) - (cancel [] - (reset! edit? false)) - (edit [] - (reset! edit? true)) - (on-input-keydown [e] - (cond - (k/esc? e) (cancel) - (k/enter? e) - (do - (dom/prevent-default e) - (dom/stop-propagation e) - (save))))] - [:div.dashboard-title - [:h2 - (if @edit? - [:div.dashboard-title-field - [:span.edit {:content-editable true - :ref input - :on-key-down on-input-keydown - :dangerouslySetInnerHTML {"__html" value}}] - [:span.close {:on-click cancel} i/close]] - (if-not read-only? - [:span.dashboard-title-field {:on-double-click edit} value] - [:span.dashboard-title-field value]))] - (when-not read-only? - [:div.edition - (if @edit? - [:span {:on-click save} i/save] - [:span {:on-click edit} i/pencil]) - [:span {:on-click on-delete} i/trash]])]))) - diff --git a/frontend/src/app/main/ui/dashboard/files.cljs b/frontend/src/app/main/ui/dashboard/files.cljs new file mode 100644 index 000000000..18b659332 --- /dev/null +++ b/frontend/src/app/main/ui/dashboard/files.cljs @@ -0,0 +1,126 @@ +;; 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/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 UXBOX Labs SL + +(ns app.main.ui.dashboard.files + (:require + [app.main.data.dashboard :as dd] + [app.main.store :as st] + [app.main.ui.components.context-menu :refer [context-menu]] + [app.main.ui.dashboard.grid :refer [grid]] + [app.main.ui.icons :as i] + [app.main.ui.keyboard :as kbd] + [app.main.ui.modal :as modal] + [app.util.dom :as dom] + [app.util.i18n :as i18n :refer [t]] + [app.util.router :as rt] + [okulary.core :as l] + [rumext.alpha :as mf])) + +(mf/defc header + [{:keys [team project] :as props}] + (let [local (mf/use-state {:menu-open false + :edition false}) + locale (mf/deref i18n/locale) + project-id (:id project) + team-id (:id team) + + on-menu-click + (mf/use-callback #(swap! local assoc :menu-open true)) + + on-menu-close + (mf/use-callback #(swap! local assoc :menu-open false)) + + on-edit + (mf/use-callback #(swap! local assoc :edition true :menu-open false)) + + on-blur + (mf/use-callback + (mf/deps project) + (fn [event] + (let [name (-> event dom/get-target dom/get-value)] + #_(st/emit! (dd/rename-project (:id project) name)) + (swap! local assoc :edition false)))) + + on-key-down + (mf/use-callback + (mf/deps project) + (fn [event] + (cond + (kbd/enter? event) (on-blur event) + (kbd/esc? event) (swap! local assoc :edition false)))) + + delete-fn + (mf/use-callback + (mf/deps project) + (fn [event] + (st/emit! (dd/delete-project project) + (rt/nav :dashboard-projects {:team-id (:id team)})))) + + on-delete + (mf/use-callback + (mf/deps project) + (fn [] (modal/show! :confirm-dialog {:on-accept delete-fn}))) + + on-create-clicked + (mf/use-callback + (mf/deps project) + (fn [event] + (dom/prevent-default event) + (st/emit! (dd/create-file (:id project)))))] + + + [:header.dashboard-header + (if (:is-default project) + [:h1.dashboard-title (t locale "dashboard.header.draft")] + [:* + [:h1.dashboard-title (t locale "dashboard.header.project" (:name project))] + [:div.icon {:on-click on-menu-click} i/actions] + [:& context-menu {:on-close on-menu-close + :show (:menu-open @local) + :options [[(t locale "dashboard.grid.rename") on-edit] + [(t locale "dashboard.grid.delete") on-delete]]}] + (if (:edition @local) + [:input.element-name {:type "text" + :auto-focus true + :on-key-down on-key-down + :on-blur on-blur + :default-value (:name project)}])]) + #_[:ul.main-nav + [:li.current + [:a "PROJECTS"]] + [:li + [:a "MEMBERS"]]] + + [:a.btn-secondary.btn-small {:on-click on-create-clicked} + (t locale "dashboard.new-file")]])) + +(defn files-ref + [project-id] + (l/derived (l/in [:files project-id]) st/state)) + +(mf/defc files-section + [{:keys [project team] :as props}] + (let [files-ref (mf/use-memo (mf/deps (:id project)) #(files-ref (:id project))) + files-map (mf/deref files-ref) + files (->> (vals files-map) + (sort-by :modified-at) + (reverse))] + + (mf/use-effect + (mf/deps (:id project)) + (fn [] + (st/emit! (dd/fetch-files {:project-id (:id project)})))) + + [:* + [:& header {:team team :project project}] + [:section.dashboard-grid-container + [:& grid {:id (:id project) + :files files + :hide-new? true}]]])) + diff --git a/frontend/src/app/main/ui/dashboard/grid.cljs b/frontend/src/app/main/ui/dashboard/grid.cljs index 348b6db64..136bd1bde 100644 --- a/frontend/src/app/main/ui/dashboard/grid.cljs +++ b/frontend/src/app/main/ui/dashboard/grid.cljs @@ -10,8 +10,9 @@ (ns app.main.ui.dashboard.grid (:require [app.common.uuid :as uuid] + [app.common.math :as mth] [app.config :as cfg] - [app.main.data.dashboard :as dsh] + [app.main.data.dashboard :as dd] [app.main.fonts :as fonts] [app.main.store :as st] [app.main.ui.components.context-menu :refer [context-menu]] @@ -62,9 +63,9 @@ (let [local (mf/use-state {:menu-open false :edition false}) locale (mf/deref i18n/locale) - delete (mf/use-callback (mf/deps id) #(st/emit! nil (dsh/delete-file id))) - add-shared (mf/use-callback (mf/deps id) #(st/emit! (dsh/set-file-shared id true))) - del-shared (mf/use-callback (mf/deps id) #(st/emit! (dsh/set-file-shared id false))) + delete (mf/use-callback (mf/deps id) #(st/emit! (dd/delete-file file))) + add-shared (mf/use-callback (mf/deps id) #(st/emit! (dd/set-file-shared id true))) + del-shared (mf/use-callback (mf/deps id) #(st/emit! (dd/set-file-shared id false))) on-close (mf/use-callback #(swap! local assoc :menu-open false)) on-delete @@ -125,8 +126,9 @@ (mf/use-callback (mf/deps id) (fn [event] - (let [name (-> event dom/get-target dom/get-value)] - (st/emit! (dsh/rename-file id name)) + (let [name (-> event dom/get-target dom/get-value) + file (assoc file :name name)] + (st/emit! (dd/rename-file file)) (swap! local assoc :edition false)))) on-key-down @@ -164,19 +166,23 @@ [(t locale "dashboard.grid.remove-shared") on-del-shared] [(t locale "dashboard.grid.add-shared") on-add-shared])]}]]])) -;; --- Grid +(mf/defc empty-placeholder + [] + (let [locale (mf/deref i18n/locale)] + [:div.grid-empty-placeholder + [:div.icon i/file-html] + [:div.text (t locale "dashboard.grid.empty-files")]])) (mf/defc grid [{:keys [id opts files hide-new?] :as props}] (let [locale (mf/deref i18n/locale) - click #(st/emit! (dsh/create-file id))] + click #(st/emit! (dd/create-file id))] [:section.dashboard-grid - (cond - (pos? (count files)) + (if (pos? (count files)) [:div.dashboard-grid-row (when (not hide-new?) [:div.grid-item.add-file {:on-click click} - [:span (t locale "ds.new-file")]]) + [:span (t locale "dashboard.new-file")]]) (for [item files] [:& grid-item @@ -184,8 +190,61 @@ :file item :key (:id item)}])] - (zero? (count files)) - [:div.grid-files-empty - [:div.grid-files-desc (t locale "dashboard.grid.empty-files")] - [:div.grid-files-link - [:a.btn-secondary.btn-small {:on-click click} (t locale "ds.new-file")]]])])) + [:& empty-placeholder])])) + +(mf/defc line-grid-row + [{:keys [locale files] :as props}] + (let [rowref (mf/use-ref) + + width (mf/use-state 900) + limit (mf/use-state 1) + itemsize 290] + + (mf/use-layout-effect + (mf/deps width) + (fn [] + (let [node (mf/ref-val rowref) + obs (new js/ResizeObserver + (fn [entries x] + (let [data (first entries) + rect (.-contentRect ^js data)] + (reset! width (.-width ^js rect))))) + + nitems (/ @width itemsize) + num (mth/floor nitems)] + + (.observe ^js obs node) + + (cond + (< (* itemsize (count files)) @width) + (reset! limit num) + + (< nitems (+ num 0.51)) + (reset! limit (dec num)) + + :else + (reset! limit num)) + (fn [] + (.disconnect ^js obs))))) + + [:div.grid-row.no-wrap {:ref rowref} + (for [item (take @limit files)] + [:& grid-item + {:id (:id item) + :file item + :key (:id item)}]) + (when (> (count files) @limit) + [:div.grid-item.placeholder + [:div.placeholder-icon i/arrow-down] + [:div.placeholder-label "Show all files"]])])) + +(mf/defc line-grid + [{:keys [project-id opts files] :as props}] + (let [locale (mf/deref i18n/locale) + click #(st/emit! (dd/create-file project-id))] + [:section.dashboard-grid + (if (pos? (count files)) + [:& line-grid-row {:files files + :locale locale}] + [:& empty-placeholder])])) + diff --git a/frontend/src/app/main/ui/dashboard/libraries.cljs b/frontend/src/app/main/ui/dashboard/libraries.cljs index 56b00b35c..e9a19a975 100644 --- a/frontend/src/app/main/ui/dashboard/libraries.cljs +++ b/frontend/src/app/main/ui/dashboard/libraries.cljs @@ -9,35 +9,34 @@ (ns app.main.ui.dashboard.libraries (:require - [okulary.core :as l] - [rumext.alpha :as mf] - [app.main.ui.icons :as i] - [app.util.i18n :as i18n :refer [tr]] - [app.util.dom :as dom] - [app.util.router :as rt] - [app.main.data.dashboard :as dsh] + [app.main.data.dashboard :as dd] [app.main.store :as st] - [app.main.ui.modal :as modal] - [app.main.ui.keyboard :as kbd] - [app.main.ui.components.context-menu :refer [context-menu]] - [app.main.ui.dashboard.grid :refer [grid]])) + [app.main.ui.dashboard.grid :refer [grid]] + [app.main.ui.icons :as i] + [app.util.dom :as dom] + [app.util.i18n :as i18n :refer [tr]] + [app.util.router :as rt] + [okulary.core :as l] + [rumext.alpha :as mf])) -(def files-ref - (-> (comp vals :files) - (l/derived st/state))) +(defn files-ref + [team-id] + (l/derived (l/in [:shared-files team-id]) st/state)) (mf/defc libraries-page - [{:keys [section team-id] :as props}] - (let [files (->> (mf/deref files-ref) - (sort-by :modified-at) - (reverse))] + [{:keys [team] :as props}] + (let [files-ref (mf/use-memo (mf/deps (:id team)) #(files-ref (:id team))) + files-map (mf/deref files-ref) + files (->> (vals files-map) + (sort-by :modified-at) + (reverse))] (mf/use-effect - (mf/deps section team-id) - #(st/emit! (dsh/initialize-libraries team-id))) + (mf/deps team) + #(st/emit! (dd/fetch-shared-files {:team-id (:id team)}))) [:* - [:header.main-bar - [:h1.dashboard-title (tr "dashboard.header.libraries")]] - [:section.libraries-page - [:& grid {:files files :hide-new? true}]]])) + [:header.dashboard-header + [:h1.dashboard-title (tr "dashboard.header.libraries")]] + [:section.dashboard-grid-container + [:& grid {:files files :hide-new? true}]]])) diff --git a/frontend/src/app/main/ui/dashboard/profile.cljs b/frontend/src/app/main/ui/dashboard/profile.cljs deleted file mode 100644 index 5d92a86be..000000000 --- a/frontend/src/app/main/ui/dashboard/profile.cljs +++ /dev/null @@ -1,57 +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/. -;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2015-2020 Andrey Antukh -;; Copyright (c) 2015-2020 Juan de la Cruz - -(ns app.main.ui.dashboard.profile - (:require - [cuerdas.core :as str] - [rumext.alpha :as mf] - [app.main.data.auth :as da] - [app.main.refs :as refs] - [app.main.store :as st] - [app.main.ui.components.dropdown :refer [dropdown]] - [app.main.ui.icons :as i] - [app.util.dom :as dom] - [app.util.i18n :as i18n :refer [t]] - [app.util.router :as rt])) - -;; --- Component: Profile - -(mf/defc profile-section - [{:keys [profile] :as props}] - (let [show (mf/use-state false) - photo (:photo-uri profile "") - photo (if (str/empty? photo) - "/images/avatar.jpg" - photo) - - locale (i18n/use-locale) - on-click - (fn [event section] - (dom/stop-propagation event) - (if (keyword? section) - (st/emit! (rt/nav section)) - (st/emit! section)))] - - [:div.user-zone {:on-click #(reset! show true)} - [:img {:src photo}] - [:span (:fullname profile)] - - [:& dropdown {:on-close #(reset! show false) - :show @show} - [:ul.profile-menu - [:li {:on-click #(on-click % :settings-profile)} - i/user - [:span (t locale "dashboard.header.profile-menu.profile")]] - [:li {:on-click #(on-click % :settings-password)} - i/lock - [:span (t locale "dashboard.header.profile-menu.password")]] - [:li {:on-click #(on-click % da/logout)} - i/exit - [:span (t locale "dashboard.header.profile-menu.logout")]]]]])) diff --git a/frontend/src/app/main/ui/dashboard/project.cljs b/frontend/src/app/main/ui/dashboard/project.cljs deleted file mode 100644 index 464490ded..000000000 --- a/frontend/src/app/main/ui/dashboard/project.cljs +++ /dev/null @@ -1,85 +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/. -;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL - -(ns app.main.ui.dashboard.project - (:require - [okulary.core :as l] - [rumext.alpha :as mf] - [app.main.ui.icons :as i] - [app.util.i18n :as i18n :refer [t]] - [app.util.dom :as dom] - [app.util.router :as rt] - [app.main.data.dashboard :as dsh] - [app.main.store :as st] - [app.main.ui.modal :as modal] - [app.main.ui.keyboard :as kbd] - [app.main.ui.components.context-menu :refer [context-menu]] - [app.main.ui.dashboard.grid :refer [grid]])) - -(def projects-ref - (l/derived :projects st/state)) - -(def files-ref - (-> (comp vals :files) - (l/derived st/state))) - -(mf/defc project-header - [{:keys [team-id project-id] :as props}] - (let [local (mf/use-state {:menu-open false - :edition false}) - projects (mf/deref projects-ref) - project (get projects project-id) - locale (i18n/use-locale) - on-menu-click #(swap! local assoc :menu-open true) - on-menu-close #(swap! local assoc :menu-open false) - on-edit #(swap! local assoc :edition true :menu-open false) - on-blur #(let [name (-> % dom/get-target dom/get-value)] - (st/emit! (dsh/rename-project project-id name)) - (swap! local assoc :edition false)) - on-key-down #(cond - (kbd/enter? %) (on-blur %) - (kbd/esc? %) (swap! local assoc :edition false)) - delete-fn #(do - (st/emit! (dsh/delete-project project-id)) - (st/emit! (rt/nav :dashboard-team {:team-id team-id}))) - on-delete #(modal/show! :confirm-dialog {:on-accept delete-fn})] - [:header.main-bar - (if (:is-default project) - [:h1.dashboard-title (t locale "dashboard.header.draft")] - [:* - [:h1.dashboard-title (t locale "dashboard.header.project" (:name project))] - [:div.main-bar-icon {:on-click on-menu-click} i/arrow-down] - [:& context-menu {:on-close on-menu-close - :show (:menu-open @local) - :options [[(t locale "dashboard.grid.rename") on-edit] - [(t locale "dashboard.grid.delete") on-delete]]}] - (if (:edition @local) - [:input.element-name {:type "text" - :auto-focus true - :on-key-down on-key-down - :on-blur on-blur - :default-value (:name project)}])]) - [:a.btn-secondary.btn-small {:on-click #(do - (dom/prevent-default %) - (st/emit! (dsh/create-file project-id)))} - (t locale "dashboard.header.new-file")]])) - -(mf/defc project-page - [{:keys [section team-id project-id] :as props}] - (let [files (->> (mf/deref files-ref) - (sort-by :modified-at) - (reverse))] - (mf/use-effect - (mf/deps section team-id project-id) - #(st/emit! (dsh/initialize-project team-id project-id))) - - [:* - [:& project-header {:team-id team-id :project-id project-id}] - [:section.projects-page - [:& grid { :id project-id :files files :hide-new? true}]]])) diff --git a/frontend/src/app/main/ui/dashboard/projects.cljs b/frontend/src/app/main/ui/dashboard/projects.cljs new file mode 100644 index 000000000..027d71dd8 --- /dev/null +++ b/frontend/src/app/main/ui/dashboard/projects.cljs @@ -0,0 +1,138 @@ +;; 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/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 UXBOX Labs SL + +(ns app.main.ui.dashboard.projects + (:require + [okulary.core :as l] + [rumext.alpha :as mf] + [app.common.exceptions :as ex] + [app.main.constants :as c] + [app.main.data.dashboard :as dd] + [app.main.refs :as refs] + [app.main.store :as st] + [app.main.ui.dashboard.grid :refer [line-grid]] + [app.main.ui.icons :as i] + [app.main.ui.keyboard :as kbd] + [app.util.dom :as dom] + [app.util.i18n :as i18n :refer [t tr]] + [app.util.router :as rt] + [app.util.time :as dt])) + +;; --- Component: Recent files + +(mf/defc header + {::mf/wrap [mf/memo]} + [{:keys [profile locale team] :as props}] + (let [create #(st/emit! (dd/create-project {:team-id (:id team)}))] + [:header.dashboard-header + [:h1.dashboard-title "Projects"] + [:a.btn-secondary.btn-small {:on-click create} + (t locale "dashboard.header.new-project")]])) + +(defn files-ref + [project-id] + (l/derived (l/in [:files project-id]) st/state)) + +(defn recent-ref + [project-id] + (l/derived (l/in [:recent-files project-id]) st/state)) + +(mf/defc project-item + [{:keys [project first? locale] :as props}] + (let [files-ref (mf/use-memo (mf/deps project) #(files-ref (:id project))) + recent-ref (mf/use-memo (mf/deps project) #(recent-ref (:id project))) + + files-map (mf/deref files-ref) + recent-ids (mf/deref recent-ref) + + files (->> recent-ids + (map #(get files-map %)) + (sort-by :modified-at) + (reverse)) + + project-id (:id project) + team-id (:team-id project) + file-count (or (:count project) 0) + + on-nav + (mf/use-callback + (mf/deps project) + (fn [] + (st/emit! (rt/nav :dashboard-files {:team-id (:team-id project) + :project-id (:id project)})))) + toggle-pin + (mf/use-callback + (mf/deps project) + (fn [] + (st/emit! (dd/toggle-project-pin project)))) + + on-file-created + (mf/use-callback + (mf/deps project) + (fn [data] + (let [pparams {:project-id (:project-id data) + :file-id (:id data)} + qparams {:page-id (get-in data [:data :pages 0])}] + (st/emit! (rt/nav :workspace pparams qparams))))) + + + create-file + (mf/use-callback + (mf/deps project) + (fn [] + (let [mdata {:on-success on-file-created} + params {:project-id (:id project)}] + (st/emit! (dd/create-file (with-meta params mdata))))))] + + + [:div.dashboard-project-row {:class (when first? "first")} + [:div.project + (when-not (:is-default project) + [:span.pin-icon + {:class (when (:is-pinned project) "active") + :on-click toggle-pin} + i/pin]) + [:h2 {:on-click on-nav} (:name project)] + [:span.info (str file-count " files")] + (when (> file-count 0) + (let [time (-> (:modified-at project) + (dt/timeago {:locale locale}))] + [:span.recent-files-row-title-info (str ", " time)])) + + [:a.btn-secondary.btn-small + {:on-click create-file} + (t locale "dashboard.new-file")]] + + [:& line-grid + {:project-id (:id project) + :files files}]])) + +(mf/defc projects-section + [{:keys [team projects] :as props}] + (let [projects (->> (vals projects) + (sort-by :modified-at) + (reverse)) + locale (mf/deref i18n/locale)] + + (mf/use-effect + (mf/deps team) + (fn [] + (st/emit! (dd/fetch-recent-files {:team-id (:id team)})))) + + (when (seq projects) + [:* + [:& header {:locale locale + :team team}] + [:section.dashboard-grid-container + (for [project projects] + [:& project-item {:project project + :locale locale + :first? (= project (first projects)) + :key (:id project)}])]]))) + diff --git a/frontend/src/app/main/ui/dashboard/recent_files.cljs b/frontend/src/app/main/ui/dashboard/recent_files.cljs deleted file mode 100644 index 473e3a912..000000000 --- a/frontend/src/app/main/ui/dashboard/recent_files.cljs +++ /dev/null @@ -1,95 +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/. -;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL - -(ns app.main.ui.dashboard.recent-files - (:require - [okulary.core :as l] - [rumext.alpha :as mf] - [app.common.exceptions :as ex] - [app.main.constants :as c] - [app.main.data.dashboard :as dsh] - [app.main.refs :as refs] - [app.main.store :as st] - [app.main.ui.dashboard.grid :refer [grid]] - [app.main.ui.icons :as i] - [app.main.ui.keyboard :as kbd] - [app.util.dom :as dom] - [app.util.i18n :as i18n :refer [t tr]] - [app.util.router :as rt] - [app.util.time :as dt])) - -;; --- Component: Content - -(def projects-ref - (l/derived :projects st/state)) - -(def recent-file-ids-ref - (l/derived :recent-file-ids st/state)) - -(def files-ref - (l/derived :files st/state)) - -;; --- Component: Recent files - -(mf/defc recent-files-header - [{:keys [profile] :as props}] - (let [locale (i18n/use-locale)] - [:header#main-bar.main-bar - [:h1.dashboard-title "Recent"] - [:a.btn-secondary.btn-small {:on-click #(st/emit! dsh/create-project)} - (t locale "dashboard.header.new-project")]])) - -(mf/defc recent-project - [{:keys [project files first? locale] :as props}] - (let [project-id (:id project) - team-id (:team-id project) - file-count (or (:file-count project) 0)] - [:div.recent-files-row - {:class-name (when first? "first")} - [:div.recent-files-row-title - [:h2.recent-files-row-title-name {:on-click #(st/emit! (rt/nav :dashboard-project {:team-id team-id - :project-id project-id})) - :style {:cursor "pointer"}} (:name project)] - [:span.recent-files-row-title-info (str file-count " files")] - (when (> file-count 0) - (let [time (-> (:modified-at project) - (dt/timeago {:locale locale}))] - [:span.recent-files-row-title-info (str ", " time)]))] - [:& grid {:id (:id project) - :files files - :hide-new? true}]])) - - -(mf/defc recent-files-page - [{:keys [team-id] :as props}] - (let [projects (->> (mf/deref projects-ref) - (vals) - (sort-by :modified-at) - (reverse)) - files (mf/deref files-ref) - recent-file-ids (mf/deref recent-file-ids-ref) - locale (i18n/use-locale) - setup #(st/emit! (dsh/initialize-recent team-id))] - - (-> (mf/deps team-id) - (mf/use-effect #(st/emit! (dsh/initialize-recent team-id)))) - - (when (and projects recent-file-ids) - [:* - [:& recent-files-header] - [:section.recent-files-page - (for [project projects] - [:& recent-project {:project project - :locale locale - :key (:id project) - :files (->> (get recent-file-ids (:id project)) - (map #(get files %)) - (filter identity)) ;; avoid failure if a "project only" files list is in global state - :first? (= project (first projects))}])]]))) - diff --git a/frontend/src/app/main/ui/dashboard/search.cljs b/frontend/src/app/main/ui/dashboard/search.cljs index 6a3eb914c..d32652d6b 100644 --- a/frontend/src/app/main/ui/dashboard/search.cljs +++ b/frontend/src/app/main/ui/dashboard/search.cljs @@ -5,47 +5,50 @@ ;; This Source Code Form is "Incompatible With Secondary Licenses", as ;; defined by the Mozilla Public License, v. 2.0. ;; -;; Copyright (c) 2015-2017 Juan de la Cruz -;; Copyright (c) 2015-2020 Andrey Antukh +;; Copyright (c) 2020 UXBOX Labs SL (ns app.main.ui.dashboard.search (:require - [okulary.core :as l] - [rumext.alpha :as mf] + [app.main.data.dashboard :as dd] [app.main.store :as st] - [app.main.data.dashboard :as dsh] + [app.main.ui.dashboard.grid :refer [grid]] + [app.main.ui.icons :as i] [app.util.i18n :as i18n :refer [t]] - [app.main.ui.dashboard.grid :refer [grid]])) + [okulary.core :as l] + [rumext.alpha :as mf])) ;; --- Component: Search -(def search-result-ref - (-> #(get-in % [:dashboard-local :search-result]) - (l/derived st/state))) +(def result-ref + (l/derived (l/in [:dashboard-local :search-result]) st/state)) (mf/defc search-page - [{:keys [team-id search-term] :as props}] - (let [search-result (mf/deref search-result-ref) - locale (i18n/use-locale)] + [{:keys [team search-term] :as props}] + (let [result (mf/deref result-ref) + locale (mf/deref i18n/locale)] + (mf/use-effect - (mf/deps search-term) - #(st/emit! (dsh/initialize-search team-id search-term))) + (mf/deps team search-term) + (st/emitf (dd/search-files {:team-id (:id team) + :search-term search-term}))) - [:section.search-page - [:section.dashboard-grid - (cond - (empty? search-term) - [:div.grid-files-empty - [:div.grid-files-desc (t locale "dashboard.search.type-something")]] + [:section.dashboard-grid-container.search + (cond + (empty? search-term) + [:div.grid-empty-placeholder + [:div.icon i/search] + [:div.text (t locale "dashboard.search.type-something")]] - (nil? search-result) - [:div.grid-files-empty - [:div.grid-files-desc (t locale "dashboard.search.searching-for" search-term)]] + (nil? result) + [:div.grid-empty-placeholder + [:div.icon i/search] + [:div.text (t locale "dashboard.search.searching-for" search-term)]] - (empty? search-result) - [:div.grid-files-empty - [:div.grid-files-desc (t locale "dashboard.search.no-matches-for" search-term)]] - - :else - [:& grid { :files search-result :hide-new? true}])]])) + (empty? result) + [:div.grid-empty-placeholder + [:div.icon i/search] + [:div.text (t locale "dashboard.search.no-matches-for" search-term)]] + :else + [:& grid {:files result + :hide-new? true}])])) diff --git a/frontend/src/app/main/ui/dashboard/sidebar.cljs b/frontend/src/app/main/ui/dashboard/sidebar.cljs index 10d6b2e61..5affb40d1 100644 --- a/frontend/src/app/main/ui/dashboard/sidebar.cljs +++ b/frontend/src/app/main/ui/dashboard/sidebar.cljs @@ -5,195 +5,389 @@ ;; This Source Code Form is "Incompatible With Secondary Licenses", as ;; defined by the Mozilla Public License, v. 2.0. ;; -;; Copyright (c) 2020 Andrey Antukh -;; Copyright (c) 2020 Juan de la Cruz +;; Copyright (c) 2020 UXBOX Labs SL (ns app.main.ui.dashboard.sidebar (:require + [app.common.data :as d] + [app.common.spec :as us] + [app.main.constants :as c] + [app.main.data.auth :as da] + [app.main.data.dashboard :as dd] + [app.main.data.messages :as dm] + [app.main.refs :as refs] + [app.main.repo :as rp] + [app.main.store :as st] + [app.main.ui.components.dropdown :refer [dropdown]] + [app.main.ui.components.forms :refer [input submit-button form]] + [app.main.ui.icons :as i] + [app.main.ui.keyboard :as kbd] + [app.main.ui.modal :as modal] + [app.util.dom :as dom] + [app.util.i18n :as i18n :refer [t tr]] + [app.util.object :as obj] + [app.util.router :as rt] + [app.util.time :as dt] + [beicon.core :as rx] + [cljs.spec.alpha :as s] [cuerdas.core :as str] [goog.functions :as f] [okulary.core :as l] - [rumext.alpha :as mf] - [app.main.constants :as c] - [app.main.data.dashboard :as dsh] - [app.main.refs :as refs] - [app.main.store :as st] - [app.main.ui.dashboard.common :as common] - [app.main.ui.icons :as i] - [app.main.ui.keyboard :as kbd] - [app.util.dom :as dom] - [app.util.i18n :as i18n :refer [t tr]] - [app.util.router :as rt] - [app.util.time :as dt])) + [rumext.alpha :as mf])) -;; --- Component: Sidebar +(mf/defc sidebar-project-edition + [{:keys [item on-end] :as props}] + (let [name (mf/use-state (:name item)) + input-ref (mf/use-ref) -(mf/defc sidebar-project - [{:keys [id name selected? team-id] :as props}] - (let [dashboard-local @refs/dashboard-local - project-for-edit (:project-for-edit dashboard-local) - local (mf/use-state {:name name - :editing (= id project-for-edit)}) - editable? (not (nil? id)) - edit-input-ref (mf/use-ref) + on-input + (mf/use-callback + (fn [event] + (->> event + (dom/get-target) + (dom/get-value) + (reset! name)))) - on-click #(st/emit! (rt/nav :dashboard-project {:team-id team-id :project-id id})) - on-dbl-click #(when editable? (swap! local assoc :editing true)) - on-input #(as-> % $ - (dom/get-target $) - (dom/get-value $) - (swap! local assoc :name $)) - on-cancel #(do - (st/emit! dsh/clear-project-for-edit) - (swap! local assoc :editing false :name name)) - on-keyup #(cond - (kbd/esc? %) - (on-cancel) + on-cancel + (mf/use-callback + (fn [] + (st/emit! dd/clear-project-for-edit) + (on-end))) - (kbd/enter? %) - (let [name (-> % dom/get-target dom/get-value)] - (st/emit! dsh/clear-project-for-edit) - (st/emit! (dsh/rename-project id name)) - (swap! local assoc :editing false)))] + on-keyup + (mf/use-callback + (fn [event] + (cond + (kbd/esc? event) + (on-cancel) + + (kbd/enter? event) + (let [name (-> event + dom/get-target + dom/get-value)] + (st/emit! dd/clear-project-for-edit + (dd/rename-project (assoc item :name name))) + (on-end)))))] (mf/use-effect - (mf/deps (:editing @local)) - #(when (:editing @local) - (let [edit-input (mf/ref-val edit-input-ref)] - (dom/focus! edit-input) - (dom/select-text! edit-input)) - nil)) + (fn [] + (let [node (mf/ref-val input-ref)] + (dom/focus! node) + (dom/select-text! node)))) + + [:div.edit-wrapper + [:input.element-title {:value @name + :ref input-ref + :on-change on-input + :on-key-down on-keyup}] + [:span.close {:on-click on-cancel} i/close]])) + + + +(mf/defc sidebar-project + [{:keys [item selected?] :as props}] + (let [dstate (mf/deref refs/dashboard-local) + edit-id (:project-for-edit dstate) + + edition? (mf/use-state (= (:id item) edit-id)) + + on-click + (mf/use-callback + (mf/deps item) + (fn [] + (st/emit! (rt/nav :dashboard-files {:team-id (:team-id item) + :project-id (:id item)})))) + on-dbl-click + (mf/use-callback #(reset! edition? true))] [:li {:on-click on-click :on-double-click on-dbl-click - :class-name (when selected? "current")} - (if (:editing @local) - [:div.edit-wrapper - [:input.element-title {:value (:name @local) - :ref edit-input-ref - :on-change on-input - :on-key-down on-keyup}] - [:span.close {:on-click on-cancel} i/close]] - [:* - i/folder - [:span.element-title name]])])) - -(def projects-iref - (l/derived :projects st/state)) - -(mf/defc sidebar-projects - [{:keys [team-id selected-project-id] :as props}] - (let [projects (->> (mf/deref projects-iref) - (vals) - (remove #(:is-default %)) - (sort-by :created-at))] - (for [item projects] - [:& sidebar-project - {:id (:id item) - :key (:id item) - :name (:name item) - :selected? (= (:id item) selected-project-id) - :team-id team-id - }]))) - -(mf/defc sidebar-team - [{:keys [profile - team-id - selected-section - selected-project-id - selected-team-id] :as props}] - (let [home? (and (= selected-section :dashboard-team) - (= selected-team-id (:default-team-id profile))) - drafts? (and (= selected-section :dashboard-project) - (= selected-team-id (:default-team-id profile)) - (= selected-project-id (:default-project-id profile))) - libraries? (= selected-section :dashboard-libraries) - ;; library? (and (str/starts-with? (name selected-section) "dashboard-library") - ;; (= selected-team-id (:default-team-id profile))) - locale (i18n/use-locale)] - [:div.sidebar-team - [:ul.dashboard-elements.dashboard-common - [:li.recent-projects - {:on-click #(st/emit! (rt/nav :dashboard-team {:team-id team-id})) - :class-name (when home? "current")} - i/recent - [:span.element-title (t locale "dashboard.sidebar.recent")]] - - [:li - {:on-click #(st/emit! (rt/nav :dashboard-project {:team-id team-id - :project-id "drafts"})) - :class-name (when drafts? "current")} - i/file-html - [:span.element-title (t locale "dashboard.sidebar.drafts")]] - - [:li - {:on-click #(st/emit! (rt/nav :dashboard-libraries {:team-id team-id})) - :class-name (when libraries? "current")} - i/library - [:span.element-title (t locale "dashboard.sidebar.libraries")]]] - - [:div.projects-row - [:span "PROJECTS"] - [:a.btn-icon-light.btn-small {:on-click #(st/emit! dsh/create-project)} - i/close]] - - [:ul.dashboard-elements - [:& sidebar-projects - {:selected-team-id selected-team-id - :selected-project-id selected-project-id - :team-id team-id}]]] - - )) + :class (when selected? "current")} + (if @edition? + [:& sidebar-project-edition {:item item + :on-end #(reset! edition? false)}] + [:span.element-title (:name item)])])) -(def debounced-emit! (f/debounce st/emit! 500)) +(mf/defc sidebar-search + [{:keys [search-term team-id locale] :as props}] + (let [search-term (or search-term "") -(mf/defc sidebar - [{:keys [section team-id project-id search-term] :as props}] - (let [locale (i18n/use-locale) - profile (mf/deref refs/profile) - search-term-not-nil (or search-term "") + emit! (mf/use-memo #(f/debounce st/emit! 500)) on-search-focus - (fn [event] - (let [target (dom/get-target event) - value (dom/get-value target)] - (dom/select-text! target) - (if (empty? value) - (debounced-emit! (rt/nav :dashboard-search {:team-id team-id} {})) - (debounced-emit! (rt/nav :dashboard-search {:team-id team-id} {:search-term value}))))) + (mf/use-callback + (mf/deps team-id) + (fn [event] + (let [target (dom/get-target event) + value (dom/get-value target)] + (dom/select-text! target) + (if (empty? value) + (emit! (rt/nav :dashboard-search {:team-id team-id} {})) + (emit! (rt/nav :dashboard-search {:team-id team-id} {:search-term value})))))) on-search-change - (fn [event] - (let [value (-> (dom/get-target event) - (dom/get-value))] - (debounced-emit! (rt/nav :dashboard-search {:team-id team-id} {:search-term value})))) + (mf/use-callback + (mf/deps team-id) + (fn [event] + (let [value (-> (dom/get-target event) + (dom/get-value))] + (emit! (rt/nav :dashboard-search {:team-id team-id} {:search-term value}))))) on-clear-click - (fn [event] - (let [search-input (dom/get-element "search-input")] - (dom/clean-value! search-input) - (dom/focus! search-input) - (debounced-emit! (rt/nav :dashboard-search {:team-id team-id} {}))))] + (mf/use-callback + (mf/deps team-id) + (fn [event] + (let [search-input (dom/get-element "search-input")] + (dom/clean-value! search-input) + (dom/focus! search-input) + (emit! (rt/nav :dashboard-search {:team-id team-id} {})))))] + + [:form.sidebar-search + [:input.input-text + {:key :images-search-box + :id "search-input" + :type "text" + :placeholder (t locale "ds.search.placeholder") + :default-value search-term + :auto-complete "off" + :on-focus on-search-focus + :on-change on-search-change + :ref #(when % (set! (.-value %) search-term))}] + [:div.clear-search + {:on-click on-clear-click} + i/close]])) + +(mf/defc sidebar-team-switch + [{:keys [team profile] :as props}] + (let [show-dropdown? (mf/use-state false) + + show-team-opts-ddwn? (mf/use-state false) + show-teams-ddwn? (mf/use-state false) + teams (mf/use-state []) + + on-nav + (mf/use-callback #(st/emit! (rt/nav :dashboard-projects {:team-id %}))) + + on-create-clicked + (mf/use-callback #(modal/show! :team-form {}))] + + (mf/use-effect + (mf/deps (:id teams)) + (fn [] + (->> (rp/query! :teams) + (rx/subs #(reset! teams %))))) + + [:div.sidebar-team-switch + [:div.switch-content + [:div.current-team + [:div.team-name + [:span.team-icon i/logo-icon] + (if (:is-default team) + [:span.team-text "Your penpot"] + [:span.team-text (:name team)])] + [:span.switch-icon {:on-click #(reset! show-teams-ddwn? true)} + i/arrow-down]] + (when-not (:is-default team) + [:div.switch-options {:on-click #(reset! show-team-opts-ddwn? true)} + i/actions])] + + ;; Teams Dropdown + [:& dropdown {:show @show-teams-ddwn? + :on-close #(reset! show-teams-ddwn? false)} + [:ul.dropdown.teams-dropdown + [:li.title "Switch Team"] + [:hr] + [:li.team-item {:on-click (partial on-nav (:default-team-id profile))} + [:span.icon i/logo-icon] + [:span.text "Your penpot"]] + + (for [team (remove :is-default @teams)] + [:* {:key (:id team)} + [:hr] + [:li.team-item {:on-click (partial on-nav (:id team))} + [:span.icon i/logo-icon] + [:span.text (:name team)]]]) + + [:hr] + [:li.action {:on-click on-create-clicked} + "+ Create new team"]]] + + [:& dropdown {:show @show-team-opts-ddwn? + :on-close #(reset! show-team-opts-ddwn? false)} + [:ul.dropdown.options-dropdown + [:li "Members"] + [:li "Settings"] + [:hr] + [:li "Rename"] + [:li "Leave team"] + [:li "Delete team"]]] + ])) + +(s/def ::name ::us/not-empty-string) +(s/def ::team-form + (s/keys :req-un [::name])) + +(mf/defc team-form-modal + {::mf/register modal/components + ::mf/register-as :team-form} + [props] + (let [locale (mf/deref i18n/locale) + + on-success + (mf/use-callback + (fn [form response] + (modal/hide!) + (let [msg "Team created successfuly"] + (st/emit! + (dm/success msg) + (rt/nav :dashboard-projects {:team-id (:id response)}))))) + + on-error + (mf/use-callback + (fn [form response] + (let [msg "Error on creating team."] + (st/emit! (dm/error msg))))) + + on-submit + (mf/use-callback + (fn [form] + (let [mdata {:on-success (partial on-success form) + :on-error (partial on-error form)} + params {:name (get-in form [:clean-data :name])}] + (st/emit! (dd/create-team (with-meta params mdata))))))] + + [:div.modal-overlay + [:div.generic-modal.team-form-modal + [:span.close {:on-click #(modal/hide!)} i/close] + [:section.modal-content.generic-form + [:h2 "CREATE NEW TEAM"] + + [:& form {:on-submit on-submit + :spec ::team-form + :initial {}} + + [:& input {:type "text" + :name :name + :label "Enter new team name:"}] + + [:div.buttons-row + [:& submit-button + {:label "Create team"}]]]]]])) + + +(mf/defc sidebar-content + [{:keys [locale projects profile section team project search-term] :as props}] + (let [default-project-id + (->> (vals projects) + (d/seek :is-default) + (:id)) + + team-id (:id team) + projects? (= section :dashboard-projects) + libs? (= section :dashboard-libraries) + drafts? (and (= section :dashboard-files) + (= (:id project) default-project-id)) + + go-projects #(st/emit! (rt/nav :dashboard-projects {:team-id (:id team)})) + go-default #(st/emit! (rt/nav :dashboard-files {:team-id (:id team) :project-id default-project-id})) + go-libs #(st/emit! (rt/nav :dashboard-libraries {:team-id (:id team)})) + + pinned-projects + (->> (vals projects) + (remove :is-default) + (filter :is-pinned))] + + [:div.sidebar-content + [:& sidebar-team-switch {:team team :profile profile}] + + [:hr] + [:& sidebar-search {:search-term search-term + :team-id (:id team) + :locale locale}] + [:div.sidebar-content-section + [:ul.sidebar-nav.no-overflow + [:li.recent-projects + {:on-click go-projects + :class-name (when projects? "current")} + i/recent + [:span.element-title (t locale "dashboard.sidebar.projects")]] + + [:li {:on-click go-default + :class-name (when drafts? "current")} + i/file-html + [:span.element-title (t locale "dashboard.sidebar.drafts")]] + + + [:li {:on-click go-libs + :class-name (when libs? "current")} + i/library + [:span.element-title (t locale "dashboard.sidebar.libraries")]]]] + + [:hr] + + [:div.sidebar-content-section + (if (seq pinned-projects) + [:ul.sidebar-nav + (for [item pinned-projects] + [:& sidebar-project + {:item item + :id (:id item) + :selected? (= (:id item) (:id project))}])] + [:div.sidebar-empty-placeholder + [:span.icon i/pin] + [:span.text "Pinned projects will appear here"]])]])) + + +(mf/defc profile-section + [{:keys [profile locale] :as props}] + (let [show (mf/use-state false) + photo (:photo-uri profile "") + photo (if (str/empty? photo) + "/images/avatar.jpg" + photo) + + on-click + (mf/use-callback + (fn [section event] + (dom/stop-propagation event) + (if (keyword? section) + (st/emit! (rt/nav section)) + (st/emit! section))))] + + [:div.profile-section {:on-click #(reset! show true)} + [:img {:src photo}] + [:span (:fullname profile)] + + [:& dropdown {:on-close #(reset! show false) + :show @show} + [:ul.dropdown + [:li {:on-click (partial on-click :settings-profile)} + [:span.icon i/user] + [:span.text (t locale "dashboard.header.profile-menu.profile")]] + [:hr] + [:li {:on-click (partial on-click :settings-password)} + [:span.icon i/lock] + [:span.text (t locale "dashboard.header.profile-menu.password")]] + [:hr] + [:li {:on-click (partial on-click da/logout)} + [:span.icon i/exit] + [:span.text (t locale "dashboard.header.profile-menu.logout")]]]]])) + +(mf/defc sidebar + {::mf/wrap-props false + ::mf/wrap [mf/memo]} + [props] + (let [locale (mf/deref i18n/locale) + profile (mf/deref refs/profile) + props (-> (obj/clone props) + (obj/set! "locale" locale) + (obj/set! "profile" profile))] [:div.dashboard-sidebar - [:div.dashboard-sidebar-inside - [:form.dashboard-search - [:input.input-text - {:key :images-search-box - :id "search-input" - :type "text" - :placeholder (t locale "ds.search.placeholder") - :default-value search-term-not-nil - :auto-complete "off" - :on-focus on-search-focus - :on-change on-search-change - :ref #(when % (set! (.-value %) search-term-not-nil))}] - [:div.clear-search - {:on-click on-clear-click} - i/close]] - [:& sidebar-team {:selected-team-id team-id - :selected-project-id project-id - :selected-section section - :profile profile - :team-id (:default-team-id profile)}]]])) + [:div.sidebar-inside + [:> sidebar-content props] + [:& profile-section {:profile profile + :locale locale}]]])) + + diff --git a/frontend/src/app/main/ui/settings.cljs b/frontend/src/app/main/ui/settings.cljs index eb3ba01ee..d709fec77 100644 --- a/frontend/src/app/main/ui/settings.cljs +++ b/frontend/src/app/main/ui/settings.cljs @@ -16,7 +16,6 @@ [app.main.refs :as refs] [app.main.store :as st] [app.util.router :as rt] - [app.main.ui.dashboard.profile :refer [profile-section]] [app.main.ui.settings.header :refer [header]] [app.main.ui.settings.password :refer [password-page]] [app.main.ui.settings.options :refer [options-page]] diff --git a/frontend/src/app/main/ui/workspace/header.cljs b/frontend/src/app/main/ui/workspace/header.cljs index ae5673899..c5b4e90f8 100644 --- a/frontend/src/app/main/ui/workspace/header.cljs +++ b/frontend/src/app/main/ui/workspace/header.cljs @@ -215,7 +215,7 @@ (mf/defc header [{:keys [file layout project page-id] :as props}] (let [team-id (:team-id project) - go-back #(st/emit! (rt/nav :dashboard-team {:team-id team-id})) + go-back #(st/emit! (rt/nav :dashboard-projects {:team-id team-id})) zoom (mf/deref refs/selected-zoom) locale (mf/deref i18n/locale) router (mf/deref refs/router) diff --git a/frontend/src/app/util/object.cljs b/frontend/src/app/util/object.cljs index e591eea5c..def453015 100644 --- a/frontend/src/app/util/object.cljs +++ b/frontend/src/app/util/object.cljs @@ -9,7 +9,7 @@ (ns app.util.object "A collection of helpers for work with javascript objects." - (:refer-clojure :exclude [set! get get-in assoc!]) + (:refer-clojure :exclude [set! get get-in merge clone]) (:require [cuerdas.core :as str] [goog.object :as gobj] @@ -44,12 +44,22 @@ :else (throw (js/Error. "unexpected input")))] (omit obj keys))) +(defn clone + [a] + (js/Object.assign #js {} a)) + (defn merge! ([a b] (js/Object.assign a b)) ([a b & more] (reduce merge! (merge! a b) more))) +(defn merge + ([a b] + (js/Object.assign #js {} a b)) + ([a b & more] + (reduce merge! (merge a b) more))) + (defn set! [obj key value] (unchecked-set obj key value)