mirror of
https://github.com/penpot/penpot.git
synced 2025-04-06 03:51:21 -05:00
♻️ Refactor dashboard (add teams)
This commit is contained in:
parent
47d347f357
commit
b3252ec2b2
52 changed files with 1842 additions and 1421 deletions
backend/src/app
db.cljmigrations.clj
migrations/sql
0002-add-profile-tables.sql0003-add-project-tables.sql0028-add-team-project-profile-rel-table.sql0029-del-project-profile-rel-indexes.sql0030-mod-file-table-add-missing-index.sql
services
frontend
|
@ -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 <niwi@niwi.nz>
|
||||
;; 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"
|
||||
|
|
|
@ -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")}
|
||||
|
||||
]})
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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)
|
||||
);
|
|
@ -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;
|
|
@ -0,0 +1 @@
|
|||
CREATE INDEX IF NOT EXISTS file__project_id__idx ON file (project_id);
|
|
@ -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)
|
||||
|
|
|
@ -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})))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)))
|
||||
|
|
|
@ -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))))
|
||||
|
||||
|
||||
|
|
|
@ -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])))
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
$color-white: #ffffff;
|
||||
$color-black: #000000;
|
||||
$color-canvas: #E8E9EA;
|
||||
$color-dashboard: #F6F6F6;
|
||||
|
||||
// Main color
|
||||
$color-primary: #31EFB8;
|
||||
|
|
|
@ -24,6 +24,7 @@ $size-6: 2rem;
|
|||
$br-small: 3px;
|
||||
$br-medium: 5px;
|
||||
$br-big: 8px;
|
||||
$br-huge: 12px;
|
||||
|
||||
// Alignments
|
||||
.text-left {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 <niwi@niwi.nz>
|
||||
// Copyright (c) 2016 Juan de la Cruz <delacruzgarciajuan@gmail.com>
|
||||
// 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';
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
.libraries-page {
|
||||
padding: 1rem;
|
||||
background-color: $color-white;
|
||||
flex: 1 0 0;
|
||||
overflow-y: auto;
|
||||
}
|
|
@ -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 <niwi@niwi.nz>
|
||||
// Copyright (c) 2015-2016 Juan de la Cruz <delacruzgarciajuan@gmail.com>
|
||||
// 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;
|
||||
}
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
.projects-page {
|
||||
padding: 1rem;
|
||||
background-color: $color-white;
|
||||
flex: 1 0 0;
|
||||
overflow-y: auto;
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
.search-page {
|
||||
padding: 1rem;
|
||||
background-color: $color-white;
|
||||
flex: 1 0 0;
|
||||
overflow-y: auto;
|
||||
}
|
|
@ -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 <niwi@niwi.nz>
|
||||
// Copyright (c) 2015-2016 Juan de la Cruz <delacruzgarciajuan@gmail.com>
|
||||
// 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;
|
||||
|
|
|
@ -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 <niwi@niwi.nz>
|
||||
// Copyright (c) 2015-2016 Juan de la Cruz <delacruzgarciajuan@gmail.com>
|
||||
// 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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 <niwi@niwi.nz>
|
||||
// Copyright (c) 2015-2016 Juan de la Cruz <delacruzgarciajuan@gmail.com>
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
100
frontend/resources/styles/main/partials/dashboard-header.scss
Normal file
100
frontend/resources/styles/main/partials/dashboard-header.scss
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 <niwi@niwi.nz>
|
||||
// Copyright (c) 2015-2016 Juan de la Cruz <delacruzgarciajuan@gmail.com>
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
76
frontend/resources/styles/main/partials/dashboard.scss
Normal file
76
frontend/resources/styles/main/partials/dashboard.scss
Normal file
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 <niwi@niwi.nz>
|
||||
// Copyright (c) 2015-2016 Juan de la Cruz <delacruzgarciajuan@gmail.com>
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -40,7 +40,7 @@
|
|||
display: flex;
|
||||
flex-grow: 1;
|
||||
flex-direction: column;
|
||||
padding: 100px;
|
||||
padding: 60px 100px;
|
||||
}
|
||||
|
||||
.button-row {
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)))))
|
||||
|
|
|
@ -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)))
|
||||
|
|
|
@ -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)})
|
||||
|
|
|
@ -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}]
|
||||
|
||||
|
|
|
@ -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}])]))
|
||||
|
||||
|
|
|
@ -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 <niwi@niwi.nz>
|
||||
|
||||
(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]])])))
|
||||
|
126
frontend/src/app/main/ui/dashboard/files.cljs
Normal file
126
frontend/src/app/main/ui/dashboard/files.cljs
Normal file
|
@ -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}]]]))
|
||||
|
|
@ -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])]))
|
||||
|
||||
|
|
|
@ -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}]]]))
|
||||
|
||||
|
|
|
@ -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 <niwi@niwi.nz>
|
||||
;; Copyright (c) 2015-2020 Juan de la Cruz <delacruzgarciajuan@gmail.com>
|
||||
|
||||
(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")]]]]]))
|
|
@ -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}]]]))
|
138
frontend/src/app/main/ui/dashboard/projects.cljs
Normal file
138
frontend/src/app/main/ui/dashboard/projects.cljs
Normal file
|
@ -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)}])]])))
|
||||
|
|
@ -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))}])]])))
|
||||
|
|
@ -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 <delacruzgarciajuan@gmail.com>
|
||||
;; Copyright (c) 2015-2020 Andrey Antukh <niwi@niwi.nz>
|
||||
;; 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}])]))
|
||||
|
|
|
@ -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 <niwi@niwi.nz>
|
||||
;; Copyright (c) 2020 Juan de la Cruz <delacruzgarciajuan@gmail.com>
|
||||
;; 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}]]]))
|
||||
|
||||
|
||||
|
|
|
@ -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]]
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Reference in a new issue