diff --git a/client/assets/icons/tag.svg b/client/assets/icons/tag.svg new file mode 100644 index 00000000..2ae3a3f6 --- /dev/null +++ b/client/assets/icons/tag.svg @@ -0,0 +1,4 @@ + + + + diff --git a/client/components/icon.js b/client/components/icon.js index 63ee6695..ffaf04f0 100644 --- a/client/components/icon.js +++ b/client/components/icon.js @@ -65,6 +65,8 @@ export const Icon = (props) => { img = "/assets/icons/delete.svg"; } else if (props.name === "share") { img = "/assets/icons/share.svg"; + } else if (props.name === "tag") { + img = "/assets/icons/tag.svg"; } else if (props.name === "bucket") { img = img_bucket; } else if (props.name === "download_white") { diff --git a/client/model/tags.js b/client/model/tags.js index 054eb493..c26c420a 100644 --- a/client/model/tags.js +++ b/client/model/tags.js @@ -1,56 +1,61 @@ import { cache, currentShare, currentBackend } from "../helpers/"; class TagManager { - all(tagPath = "/", maxSize = -1) { + all(tagPath = "/") { return cache.get(cache.FILE_TAG, [currentBackend(), currentShare()]).then((DB) => { if (DB === null) { return []; } - - if (tagPath == "/") { - const scoreFn = (acc, el) => (acc + el.replace(/[^\/]/g, "").length); - const tags = Object.keys(DB.tags).sort((a, b) => { - if (DB.tags[a].length === DB.tags[b].length) { - return DB.tags[a].reduce(scoreFn, 0) - DB.tags[b].reduce(scoreFn, 0); - } - return DB.tags[a].length < DB.tags[b].length ? 1 : -1; - }); - if(tags.length === 0) { - return ["Bookmark"]; - } else if(tags.length >= 5) { - return ["All"].concat(tags.slice(0, 5)); - } - return tags; + const tags = this._tagPathStringToArray(tagPath); + if (tags.length === 0) { + return Object.keys(DB.tags); } - return [ - // "Bookmark", "wiki", "B", "C", "D", "E", "F" - ]; + + // STEP1: build the graph of selected tags + + // STEP2: build the node that connects to the initial graph + return Object.keys(DB.tags) + .map((tag) => { + if (tags.indexOf(tag) !== -1) { // ignore tag that are already selected + return { tag, scrore: 0 }; + } + return { + tag, + score: DB.tags[tag].reduce((path, acc) => { + // TODO + return acc; + }, 0), + } + }) + .filter((t) => t && t.score > 0) + .sort((a, b) => a.score > b.score) + .map((d) => d.tag); }); } files(tagPath) { - const tags = this._tagPathStringToArray(tagPath, false); - if (tags.length === 0) return Promise.resolve([]); - else if (tags.length > 1) return Promise.resolve([]); // TODO + let tags = this._tagPathStringToArray(tagPath); return cache.get(cache.FILE_TAG, [currentBackend(), currentShare()]).then((DB) => { - if(!DB) return []; - switch(tags[0]) { - case "All": - return this.all() - .then((tags) => (tags.reduce((acc, el) => { - return DB.tags[el] ? acc.concat(DB.tags[el]) : acc; - }, []))); - default: - return Promise.resolve(DB.tags[tags[0]] || []); + if (!DB) return []; + else if (!DB.tags) return []; + else if (tags.length === 0) tags = Object.keys(DB.tags); + + // push all the candidates in an array + let paths = (DB.tags[tags[0]] || []).map((t) => ({path: t, tag: tags[0]})); + for (let i=1; i ({path: t, tag: tags[i]}))); } + + // mark element of the array that shouldn't be here + return paths; }); } - _tagPathStringToArray(tagPathString, removeFirst = true) { - return tagPathString - .split("/") - .filter((r) => r !== "" && (removeFirst ? r !== "All" : true)); + _tagPathStringToArray(tagPathString) { + return tagPathString.split("/").filter((r) => r !== ""); } addTagToFile(tag, path) { @@ -72,13 +77,15 @@ class TagManager { DB.tags[tag].splice(idx, 1); if (DB.tags[tag].length === 0) { delete DB.tags[tag]; - delete DB.weight[tag]; } return DB; }); } import(DB) { + if(JSON.stringify(Object.keys(DB)) !== JSON.stringify(["tags", "share", "backend"])) { + return Promise.reject(new Error("Not Valid")); + } return cache.upsert(cache.FILE_TAG, [currentBackend(), currentShare()], () => { return DB; }); @@ -89,7 +96,7 @@ class TagManager { return cache.get(cache.FILE_TAG, key) .then((a) => { if (a === null) { - return {tags: {}, weight: {}, share: key[1], backend: key[0]} + return {tags: {}, share: key[1], backend: key[0]} } return a; }); diff --git a/client/pages/filespage/frequently_access.js b/client/pages/filespage/frequently_access.js index 631ebdf2..66447d2f 100644 --- a/client/pages/filespage/frequently_access.js +++ b/client/pages/filespage/frequently_access.js @@ -42,6 +42,10 @@ export function FrequentlyAccess({ files, tags }) { 0}> {t("Tag")}
+ + +
All
+ { tags && tags.map((tag, index) => { return ( diff --git a/client/pages/filespage/tag.js b/client/pages/filespage/tag.js new file mode 100644 index 00000000..1f9339ff --- /dev/null +++ b/client/pages/filespage/tag.js @@ -0,0 +1,139 @@ +import React, { useState, useRef, useEffect } from "react"; + +import { + Icon, Input, +} from "../../components/"; +import { Tags } from "../../model/"; +import { t } from "../../locales/"; +import "./tag.scss"; + +export function TagComponent({ path }) { + const [DB, setDB] = useState(null); + const [input, setInput] = useState(""); + + useEffect(() => { + Tags.export().then((db) => { + setDB(db); + }); + }, []); + + const onFormSubmit = (e) => { + if (!DB) return; + else if(!DB.tags) return; + + e.preventDefault(); + const it = input.trim().toLowerCase(); + if (it === "") return; + + const newDB = {...DB}; + newDB.tags = {}; + newDB.tags[it] = [path]; + Object.keys(DB.tags).forEach((tag) => { + newDB.tags[tag] = DB.tags[tag]; + }); + setDB(newDB); + setInput(""); + Tags.import(newDB); + }; + const onClickTag = (tagName) => { + if (!DB) return; + else if(!DB.tags) return; + console.log("CLICK ON ", tagName, "idx", DB.tags[tagName].indexOf(path)); + + const newDB = {...DB}; + if (isTagActive(tagName)) { + newDB.tags[tagName].splice(DB.tags[tagName].indexOf(path), 1); + } else { + newDB.tags[tagName].push(path); + } + setDB(newDB); + Tags.import(newDB); + }; + const onClickMoveUp = (tagName) => { + if (!DB) return; + else if(!DB.tags) return; + + const newDB = {...DB}; + const keys = Object.keys(DB.tags) || []; + const n = keys.indexOf(tagName); + if (n === 0) return; + + newDB.tags = {}; + for (let i=0; i { + if (!DB) return; + else if(!DB.tags) return; + + const newDB = {...DB}; + const keys = Object.keys(DB.tags) || []; + const n = keys.indexOf(tagName); + if (n === keys.length - 1) return; + + newDB.tags = {}; + for (let i=0; i { + const newDB = {...DB}; + delete newDB.tags[tagName]; + Tags.import(newDB); + setDB(newDB); + }; + + const isTagActive = (tagName) => { + if (!DB) return false; + else if(!DB.tags) return false; + return DB.tags[tagName].indexOf(path) !== -1; + } + + const TAGS = DB && Object.keys(DB.tags); + return ( +
+
onFormSubmit(e)}> + setInput(e.target.value)} + placeholder={t("Create a Tag")} + autoFocus /> +
+
+ { + TAGS && TAGS.length > 0 ? + Object.keys(DB.tags).map((tag) => ( +
+
onClickTag(tag)}>{ tag } { (DB.tags[tag] || []).length }
+ onClickMoveUp(tag)} /> + onClickMoveDown(tag)} /> + onClickRemove(tag)} /> +
+ )) : ( +
+
onClickTag(t("Bookmark"))}>{ t("Bookmark") }
+
+ ) + } +
+
+ ); +} diff --git a/client/pages/filespage/tag.scss b/client/pages/filespage/tag.scss new file mode 100644 index 00000000..ec183d8a --- /dev/null +++ b/client/pages/filespage/tag.scss @@ -0,0 +1,43 @@ +.component_tag { + input { + font-size: 1.2em; + } + input::placeholder { + font-weight: 100; + } + .box { + display: flex; + background: var(--bg-color); + transition: background 0.1s ease; + &.active{ background: var(--primary); color: var(--color); } + padding: 5px 10px; + border-radius: 3px; + + > div { + flex-grow: 1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + img { + width: 20px; + background: rgba(0,0,0,0.05); + border-radius: 50%; + margin-left: 5px; + &[alt="close"] { + width: 14px; + padding: 3px; + } + } + .count { + opacity: 0.5; + font-size: 0.9rem; + &:before { content: "["; } + &:after { content: "]"; } + } + } + .scroll-y { + overflow-y: auto !important; + max-height: 200px; + } +} diff --git a/client/pages/filespage/thing-existing.js b/client/pages/filespage/thing-existing.js index bbbc5a47..2ec4bcf8 100644 --- a/client/pages/filespage/thing-existing.js +++ b/client/pages/filespage/thing-existing.js @@ -8,6 +8,7 @@ import { Card, NgIf, Icon, EventEmitter, img_placeholder, Input } from "../../co import { pathBuilder, basename, filetype, prompt, alert, leftPad, getMimeType, debounce, memory } from "../../helpers/"; import { Files } from "../../model/"; import { ShareComponent } from "./share"; +import { TagComponent } from "./tag"; import { t } from "../../locales/"; @@ -227,6 +228,13 @@ class ExistingThingComponent extends React.Component { this.setState({ delete_request: false }); } + onTagRequest() { + alert.now( + , + () => {}, + ) + } + onShareRequest(filename) { alert.now( , @@ -321,6 +329,7 @@ class ExistingThingComponent extends React.Component { onClickRename={this.onRenameRequest.bind(this)} onClickDelete={this.onDeleteRequest.bind(this)} onClickShare={this.onShareRequest.bind(this)} + onClickTag={this.onTagRequest.bind(this)} is_renaming={this.state.is_renaming} can_rename={this.props.metadata.can_rename !== false} can_delete={this.props.metadata.can_delete !== false} @@ -371,6 +380,7 @@ class Filename extends React.Component { e.preventDefault(); e.stopPropagation(); this.props.onRename(this.state.filename); + return false; } onCancel() { @@ -380,6 +390,7 @@ class Filename extends React.Component { preventSelect(e) { e.preventDefault(); + e.stopPropagation(); } render() { @@ -412,7 +423,7 @@ class Filename extends React.Component {
+ onSubmit={(e) => { e.preventDefault(); e.stopPropagation(); this.onRename(e) }}> this.setState({ filename: e.target.value })} @@ -443,6 +454,11 @@ const ActionButton = (props) => { props.onClickShare(); }; + const onTag = (e) => { + e.preventDefault(); + props.onClickTag(); + } + return (
{ onClick={onRename} className="component_updater--icon" /> - - - + { + /canary/.test(location.search) ? ( + + + + ) : ( + + + + ) + } diff --git a/client/pages/tagspage.js b/client/pages/tagspage.js index e885be86..ecf48ae6 100644 --- a/client/pages/tagspage.js +++ b/client/pages/tagspage.js @@ -39,21 +39,11 @@ export function TagsPageComponent({ match }) { } const onClickRemoveFile = (file) => { - prompt.now( - t("Confirm by typing") + ": remove", - (answer) => { - if (answer !== "remove") { - return Promise.resolve(); - } - Tags.removeTagFromFile( - path.split("/").filter((r) => !!r).slice(-1)[0], - file, - ); - setRefresh(refresh + 1); - return Promise.resolve(); - }, - () => {}, + Tags.removeTagFromFile( + file.tag, + file.path, ); + setRefresh(refresh + 1); } const onClickMoreDropdown = (what) => { @@ -74,15 +64,12 @@ export function TagsPageComponent({ match }) { notify.send(t("Not Valid"), "error"); return; } - if(JSON.stringify(Object.keys(jsonObject)) !== JSON.stringify(["tags", "weight", "share", "backend"])) { - notify.send(t("Not Valid"), "error"); - return; - } setLoading(true); Tags.import(jsonObject).then(() => { setLoading(false); setRefresh(refresh + 1); }).catch((err) => { + notify.send(t(err && err.message), "error"); setLoading(false); }); }; @@ -111,7 +98,11 @@ export function TagsPageComponent({ match }) {

{ - path.split("/").filter((r) => r).map((tag, idx) => ( + path === "/" ? ( + + home + + ) : path.split("/").filter((r) => r).map((tag, idx) => ( #{tag} )) } @@ -173,15 +164,16 @@ export function TagsPageComponent({ match }) { { files && files.map((file, idx) => (
- + { e.preventDefault(); onClickRemoveFile(file)}}> - + - {basename(file)}
{file} + {basename(file.path)}
+ {path === "/" && (#{file.tag} )}{file.path}
diff --git a/client/pages/tagspage.scss b/client/pages/tagspage.scss index 78d717c8..c361c6ea 100644 --- a/client/pages/tagspage.scss +++ b/client/pages/tagspage.scss @@ -18,9 +18,15 @@ h1 { font-weight: 100; text-transform: uppercase; - margin: -7px 0 0 0; + margin: 0 0 0 0; float: left; - line-height: 30px; + font-size: 1.7rem; + line-height: 1.7rem; + a .component_icon { + float: left; + width: 25px; + height: 25px; + } } } & > .component_container { diff --git a/server/middleware/session.go b/server/middleware/session.go index 85352779..67e5a48f 100644 --- a/server/middleware/session.go +++ b/server/middleware/session.go @@ -161,8 +161,10 @@ func CanManageShare(fn func(*App, http.ResponseWriter, *http.Request)) func(ctx fn(ctx, res, req) return } + Log.Debug("middleware::session::share 'permission denied - s.CanShare[%+v] s.Backend[%s]'", s.CanShare, s.Backend) + } else { + Log.Debug("middleware::session::share 'permission denied - s.CanShare[%+v] s.Backend[%s] GenerateID[%s]'", s.CanShare, s.Backend, id) } - Log.Debug("middleware::session::share 'permission denied - s.CanShare[%+v] s.Backend[%s] GenerateID[%s]'", s.CanShare, s.Backend, id) SendErrorResult(res, ErrPermissionDenied) return }