0
Fork 0
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:
Andrey Antukh 2020-09-25 14:51:21 +02:00 committed by Alonso Torres
parent 47d347f357
commit b3252ec2b2
52 changed files with 1842 additions and 1421 deletions

View file

@ -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"

View file

@ -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")}
]})
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View file

@ -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,

View file

@ -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);

View file

@ -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)
);

View file

@ -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;

View file

@ -0,0 +1 @@
CREATE INDEX IF NOT EXISTS file__project_id__idx ON file (project_id);

View file

@ -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)

View file

@ -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})))

View file

@ -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

View file

@ -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)))

View file

@ -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))))

View file

@ -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])))

View file

@ -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",

View file

@ -9,6 +9,7 @@
$color-white: #ffffff;
$color-black: #000000;
$color-canvas: #E8E9EA;
$color-dashboard: #F6F6F6;
// Main color
$color-primary: #31EFB8;

View file

@ -24,6 +24,7 @@ $size-6: 2rem;
$br-small: 3px;
$br-medium: 5px;
$br-big: 8px;
$br-huge: 12px;
// Alignments
.text-left {

View file

@ -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;
}
}
}

View file

@ -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';

View file

@ -1,6 +0,0 @@
.libraries-page {
padding: 1rem;
background-color: $color-white;
flex: 1 0 0;
overflow-y: auto;
}

View file

@ -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;
}

View file

@ -1,6 +0,0 @@
.projects-page {
padding: 1rem;
background-color: $color-white;
flex: 1 0 0;
overflow-y: auto;
}

View file

@ -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
}

View file

@ -1,6 +0,0 @@
.search-page {
padding: 1rem;
background-color: $color-white;
flex: 1 0 0;
overflow-y: auto;
}

View file

@ -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;

View file

@ -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);

View file

@ -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;
}
}
}

View file

@ -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;
}
}

View 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;
}
}
}
}

View file

@ -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;
}
}
}
}

View 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; }
}
}
}
}

View file

@ -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;
}
}
}
}
}

View file

@ -40,7 +40,7 @@
display: flex;
flex-grow: 1;
flex-direction: column;
padding: 100px;
padding: 60px 100px;
}
.button-row {

View file

@ -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))

View file

@ -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)))))

View file

@ -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)))

View file

@ -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)})

View file

@ -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}]

View file

@ -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}])]))

View file

@ -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]])])))

View 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}]]]))

View file

@ -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])]))

View file

@ -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}]]]))

View file

@ -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")]]]]]))

View file

@ -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}]]]))

View 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)}])]])))

View file

@ -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))}])]])))

View file

@ -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}])]))

View file

@ -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}]]]))

View file

@ -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]]

View file

@ -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)

View file

@ -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)