From 6135258fd834239f9960c103a0e47a5b6191bdee Mon Sep 17 00:00:00 2001 From: Raphael Gauthier Date: Thu, 17 Aug 2023 14:25:10 +0200 Subject: [PATCH] Fixes #23285: Change the display of the new filemanager to match the previous one --- .../main/elm/sources/FileManager/Action.elm | 16 +- .../src/main/elm/sources/FileManager/Env.elm | 61 +++- .../main/elm/sources/FileManager/Model.elm | 30 +- .../main/elm/sources/FileManager/Update.elm | 50 ++- .../src/main/elm/sources/FileManager/Util.elm | 4 + .../src/main/elm/sources/FileManager/View.elm | 330 +++++++++++++++--- .../main/style/rudder/rudder-filemanager.css | 154 +++++++- .../src/main/style/rudder/rudder-main.css | 1 + .../style/rudder/rudder-technique-editor.css | 2 +- .../src/main/style/rudder/rudder-template.css | 6 +- 10 files changed, 548 insertions(+), 106 deletions(-) diff --git a/webapp/sources/rudder/rudder-web/src/main/elm/sources/FileManager/Action.elm b/webapp/sources/rudder/rudder-web/src/main/elm/sources/FileManager/Action.elm index dda5ff6dddf..35020e374e7 100644 --- a/webapp/sources/rudder/rudder-web/src/main/elm/sources/FileManager/Action.elm +++ b/webapp/sources/rudder/rudder-web/src/main/elm/sources/FileManager/Action.elm @@ -7,10 +7,14 @@ import Http exposing (Body, emptyBody, expectJson, header, request, stringBody) import Json.Decode.Pipeline exposing (required) import Json.Encode import List exposing ( map) -import FileManager.Model exposing (..) +import List.Extra + import String exposing (dropLeft) import Url.Builder exposing (string, toQuery, QueryParameter) +import FileManager.Model exposing (..) +import FileManager.Util exposing (getDirPath) + get : String -> Decoder a -> (Result Http.Error a -> msg) -> Cmd msg get url decoder handler = request @@ -47,19 +51,23 @@ upload api dir file = , tracker = Just "upload" } -listDirectory : String -> String -> Cmd Msg +listDirectory : String -> List String -> Cmd Msg listDirectory api dir = let + currentFolder = case List.Extra.last dir of + Just d -> d + Nothing -> "/" + body = Json.Encode.object [ ("action", Json.Encode.string "list") - , ("path", Json.Encode.string dir) + , ("path", Json.Encode.string (getDirPath dir)) ] in post api (Http.jsonBody body ) (Decode.at ["result"] (Decode.list fileDecoder)) - (EnvMsg << LsGotten) + (EnvMsg << (LsGotten currentFolder)) fileDecoder : Decode.Decoder FileMeta fileDecoder = diff --git a/webapp/sources/rudder/rudder-web/src/main/elm/sources/FileManager/Env.elm b/webapp/sources/rudder/rudder-web/src/main/elm/sources/FileManager/Env.elm index ee1b7cf2bd9..572ea368e1a 100644 --- a/webapp/sources/rudder/rudder-web/src/main/elm/sources/FileManager/Env.elm +++ b/webapp/sources/rudder/rudder-web/src/main/elm/sources/FileManager/Env.elm @@ -1,22 +1,25 @@ module FileManager.Env exposing (..) -import FileManager.Action exposing (..) import Browser.Dom exposing (getElement) import List exposing (filter, indexedMap, map, member, reverse) -import FileManager.Model exposing (..) +import List.Extra exposing (takeWhile) import Platform.Cmd -import FileManager.Port exposing (close) import String exposing (fromInt) import Task exposing (sequence) import Tuple exposing (first, second) -import FileManager.Util exposing (..) +import Dict exposing (Dict) + +import FileManager.Action exposing (..) +import FileManager.Port exposing (close) +import FileManager.Model exposing (..) import FileManager.Vec exposing (..) +import FileManager.Util exposing (..) handleEnvMsg : EnvMsg -> Model -> (Model, Cmd Msg) handleEnvMsg msg model = case msg of Open () -> ({ model | open = True }, Cmd.none) Close -> ({ model | open = False, selected = [] }, close []) - Accept -> ({ model | open = False, selected = [] }, close <| reverse <| map ((++) model.dir << .name) model.selected) + Accept -> ({ model | open = False, selected = [] }, close <| reverse <| map ((++) (getDirPath model.dir) << .name) model.selected) MouseDown maybe pos1 ctrl -> ( { model | mouseDown = True @@ -50,7 +53,7 @@ handleEnvMsg msg model = case msg of | mouseDown = False , drag = False , showContextMenu = case maybe of - Just file -> not <| model.dir == model.clipboardDir && member file model.clipboardFiles + Just file -> not <| (getDirPath model.dir) == model.clipboardDir && member file model.clipboardFiles Nothing -> True , selected = case maybe of Just _ -> model.selected @@ -76,14 +79,50 @@ handleEnvMsg msg model = case msg of } , case maybe of Just file -> if model.drag && (file.type_ == "dir") && (not << member file) model.selected - then move model.api model.dir model.selected <| "/" ++ file.name ++ "/" + then move model.api (getDirPath model.dir) model.selected <| "/" ++ file.name ++ "/" else Cmd.none Nothing -> Cmd.none - ) - GetLs dir -> ({ model | dir = dir, files = [], load = True }, listDirectory model.api dir) - LsGotten result -> case result of - Ok files -> ({ model | files = files, selected = [], load = False }, Cmd.none) + ) + + GetLs dir -> + let + newDir = if List.member dir model.dir then takeWhile (\d -> d /= dir) model.dir else List.append model.dir [dir] + in + ({ model | dir = newDir, files = [], load = True }, listDirectory model.api newDir) + + GetLsTree dir -> + ({ model | dir = dir, files = [], load = True }, listDirectory model.api dir) + + LsGotten folder result -> case result of + Ok files -> + let + toItems : List FileMeta -> List String + toItems fs = fs + |> List.filter (\f -> f.type_ == "dir") + |> List.map .name + |> List.sort + + childs = toItems files + + newTree = case Dict.get folder model.tree of + Just ti -> Dict.insert folder (TreeItem folder ti.parents childs) model.tree + Nothing -> + let + values = Dict.values model.tree + + parents = case List.Extra.find (\ti -> List.member folder ti.childs) values of + Just p -> List.append [p.name] p.parents + Nothing -> [] + + in + Dict.insert folder (TreeItem folder parents childs) model.tree + + filters = model.filters + newFilters = {filters | opened = folder :: filters.opened} + in + ({ model | files = files, selected = [], load = False, tree = newTree, filters = newFilters }, Cmd.none) Err _ -> (model, Cmd.none) + Refresh result -> case result of Ok () -> (model, listDirectory model.api model.dir) Err _ -> (model, Cmd.none) diff --git a/webapp/sources/rudder/rudder-web/src/main/elm/sources/FileManager/Model.elm b/webapp/sources/rudder/rudder-web/src/main/elm/sources/FileManager/Model.elm index b84079e5dd2..8d59bc4b96c 100644 --- a/webapp/sources/rudder/rudder-web/src/main/elm/sources/FileManager/Model.elm +++ b/webapp/sources/rudder/rudder-web/src/main/elm/sources/FileManager/Model.elm @@ -4,6 +4,7 @@ import Browser.Dom exposing (Element) import File exposing (File) import Http exposing (Error) import FileManager.Vec exposing (..) +import Dict exposing (Dict) type alias Flags = { api: String @@ -13,11 +14,24 @@ type alias Flags = , hasWriteRights : Bool } +type ViewMode = ListView | GridView + +type SortBy = FileName | FileSize | FileDate | FileRights + +type SortOrder = Asc | Desc + +type alias Filters = + { filter : String + , sortBy : SortBy + , sortOrder : SortOrder + , opened : List String + } + type alias Model = { api: String , thumbnailsUrl: String , downloadsUrl: String - , dir: String + , dir: List String , open: Bool , load: Bool , pos1: Vec2 @@ -41,6 +55,15 @@ type alias Model = , clipboardFiles: List FileMeta , uploadQueue: List File , hasWriteRights: Bool + , viewMode : ViewMode + , filters : Filters + , tree : Dict String TreeItem + } + +type alias TreeItem = + { name : String + , parents : List String + , childs : List String } type alias FileMeta = @@ -70,6 +93,8 @@ type Msg | Delete | UpdateApiPath String | None + | ChangeViewMode ViewMode + | UpdateFilters Filters type EnvMsg = Open () @@ -80,7 +105,8 @@ type EnvMsg | MouseMove Vec2 | MouseUp (Maybe FileMeta) Int | GetLs String - | LsGotten (Result Error (List FileMeta)) + | GetLsTree (List String) + | LsGotten String (Result Error (List FileMeta)) | Refresh (Result Error ()) | GotContent (Result Error String) diff --git a/webapp/sources/rudder/rudder-web/src/main/elm/sources/FileManager/Update.elm b/webapp/sources/rudder/rudder-web/src/main/elm/sources/FileManager/Update.elm index 2906eb67bef..c838457b7e5 100644 --- a/webapp/sources/rudder/rudder-web/src/main/elm/sources/FileManager/Update.elm +++ b/webapp/sources/rudder/rudder-web/src/main/elm/sources/FileManager/Update.elm @@ -1,25 +1,29 @@ module FileManager.Update exposing (..) -import FileManager.Action exposing (..) import Browser.Navigation exposing (reload) -import FileManager.Env exposing (handleEnvMsg) import File.Download import File.Select import List exposing (map, filter) import Http -import FileManager.Model exposing (..) import Maybe +import Dict exposing (Dict) + +import FileManager.Model exposing (..) import FileManager.Vec exposing (..) +import FileManager.Action exposing (..) +import FileManager.Env exposing (handleEnvMsg) +import FileManager.Util exposing (getDirPath) + init : Flags -> (Model, Cmd Msg) -init flags = (initModel flags, let { api, dir } = flags in listDirectory api dir) +init flags = (initModel flags, let { api, dir } = flags in listDirectory api [dir] ) initModel : Flags -> Model initModel { api, thumbnailsUrl, downloadsUrl, dir, hasWriteRights } = { api = api , thumbnailsUrl = thumbnailsUrl , downloadsUrl = downloadsUrl - , dir = dir + , dir = [ dir ] , open = False , load = False , pos1 = Vec2 0 0 @@ -43,6 +47,9 @@ initModel { api, thumbnailsUrl, downloadsUrl, dir, hasWriteRights } = , clipboardFiles = [] , uploadQueue = [] , hasWriteRights = hasWriteRights + , viewMode = GridView + , filters = Filters "" FileName Asc [] + , tree = Dict.empty } update : Msg -> Model -> (Model, Cmd Msg) @@ -58,7 +65,7 @@ update msg model = case msg of , filesAmount = List.length files + 1 , showDrop = False } - , FileManager.Action.upload model.api model.dir file + , FileManager.Action.upload model.api (getDirPath model.dir) file ) Progress progress -> ({ model | progress = progress }, Cmd.none) Cancel -> (model, reload) @@ -71,7 +78,7 @@ update msg model = case msg of }, Cmd.batch [ listDirectory model.api model.dir - , FileManager.Action.upload model.api model.dir file + , FileManager.Action.upload model.api (getDirPath model.dir) file ] ) _ -> ({ model | filesAmount = 0 }, listDirectory model.api model.dir) @@ -80,7 +87,7 @@ update msg model = case msg of let cmd = case state of - Edit file _ -> getContent model.api model.dir file + Edit file _ -> getContent model.api (getDirPath model.dir) file _ -> Cmd.none in ( { model @@ -104,21 +111,21 @@ update msg model = case msg of ConfirmNameDialog -> case model.dialogState of Closed -> (model,Cmd.none) - NewDir s -> ({ model | dialogState = Closed, load = True }, newDir model.api model.dir s) - NewFile s -> ({ model | dialogState = Closed, load = True }, newFile model.api model.dir s) - Rename f s -> ({ model | dialogState = Closed, load = True },FileManager.Action.rename model.api model.dir f.name s) - Edit file content -> ({ model | dialogState = Closed, load = True },saveContent model.api model.dir file content) + NewDir s -> ({ model | dialogState = Closed, load = True }, newDir model.api (getDirPath model.dir) s) + NewFile s -> ({ model | dialogState = Closed, load = True }, newFile model.api (getDirPath model.dir) s) + Rename f s -> ({ model | dialogState = Closed, load = True },FileManager.Action.rename model.api (getDirPath model.dir) f.name s) + Edit file content -> ({ model | dialogState = Closed, load = True },saveContent model.api (getDirPath model.dir) file content) Download -> ( { model | showContextMenu = False } , Cmd.batch - <| map (File.Download.url << (++) (model.downloadsUrl ++ "?action=download&path=" ++ model.dir) << .name) + <| map (File.Download.url << (++) (model.downloadsUrl ++ "?action=download&path=" ++ (getDirPath model.dir)) << .name) <| filter (.type_ >> (/=) "dir") model.selected ) - Cut -> ({ model | clipboardDir = model.dir, clipboardFiles = model.selected, showContextMenu = False }, Cmd.none) - Paste -> if model.dir == model.clipboardDir + Cut -> ({ model | clipboardDir = (getDirPath model.dir), clipboardFiles = model.selected, showContextMenu = False }, Cmd.none) + Paste -> if (getDirPath model.dir) == model.clipboardDir then ({ model | clipboardFiles = [], showContextMenu = False }, Cmd.none) else ( { model @@ -128,11 +135,16 @@ update msg model = case msg of } , case model.caller of Just file -> if file.type_ == "dir" - then move model.api model.clipboardDir model.clipboardFiles <| model.dir ++ file.name ++ "/" + then move model.api model.clipboardDir model.clipboardFiles <| (getDirPath model.dir) ++ file.name ++ "/" else Cmd.none - Nothing -> move model.api model.clipboardDir model.clipboardFiles model.dir + Nothing -> move model.api model.clipboardDir model.clipboardFiles (getDirPath model.dir) ) - Delete -> ({ model | showContextMenu = False, load = True }, delete model.api model.dir model.selected) - UpdateApiPath apiPath -> ({model | api = apiPath, downloadsUrl = apiPath } , listDirectory apiPath "") + Delete -> ({ model | showContextMenu = False, load = True }, delete model.api (getDirPath model.dir) model.selected) + + UpdateApiPath apiPath -> ({model | api = apiPath, downloadsUrl = apiPath } , listDirectory apiPath ["/"]) + + ChangeViewMode viewMode -> ({model | viewMode = viewMode}, Cmd.none) + + UpdateFilters filters -> ({model | filters = filters}, Cmd.none) None -> (model, Cmd.none) diff --git a/webapp/sources/rudder/rudder-web/src/main/elm/sources/FileManager/Util.elm b/webapp/sources/rudder/rudder-web/src/main/elm/sources/FileManager/Util.elm index b8ebab43fcb..a1c1bca28fa 100644 --- a/webapp/sources/rudder/rudder-web/src/main/elm/sources/FileManager/Util.elm +++ b/webapp/sources/rudder/rudder-web/src/main/elm/sources/FileManager/Util.elm @@ -45,3 +45,7 @@ isJust maybe = case maybe of button : List (Attribute msg) -> List (Html msg) -> Html msg button atts childs = Html.button (type_ "button" :: atts) childs + +getDirPath : List String -> String +getDirPath dir = + String.replace "//" "/" ((String.join "/" dir) ++ "/") \ No newline at end of file diff --git a/webapp/sources/rudder/rudder-web/src/main/elm/sources/FileManager/View.elm b/webapp/sources/rudder/rudder-web/src/main/elm/sources/FileManager/View.elm index aa533677b21..1dd58398e94 100644 --- a/webapp/sources/rudder/rudder-web/src/main/elm/sources/FileManager/View.elm +++ b/webapp/sources/rudder/rudder-web/src/main/elm/sources/FileManager/View.elm @@ -1,18 +1,23 @@ module FileManager.View exposing (..) -import FileManager.Events exposing (..) -import Html exposing (Attribute, Html, br, div, i, img, input, label, strong, text, textarea) -import Html.Attributes exposing (attribute, class, draggable, id, src, style, title, type_, value) +import Html exposing (Attribute, Html, br, div, i, input, textarea, img, label, strong, text, ul, table, thead, tbody, tr, th, td, ul, li, a) +import Html.Attributes exposing (attribute, class, draggable, id, src, style, title, type_, value, placeholder, disabled) import Html.Events exposing (onClick, onDoubleClick, onInput) import Http exposing (Progress(..)) import List exposing (head, indexedMap, isEmpty, length, map, member, range, reverse, tail) -import FileManager.Model exposing (..) +import List.Extra exposing (last) import Maybe exposing (andThen, withDefault) import String exposing (fromInt, fromFloat, join, split) import Svg exposing (svg, path, circle) import Svg.Attributes exposing (cx, cy, d, width, height, fill, r, viewBox) -import FileManager.Util exposing (button, isJust) +import NaturalOrdering as N exposing (compare) +import Dict exposing (Dict) + +import FileManager.Model exposing (..) +import FileManager.Util exposing (button, isJust, getDirPath) import FileManager.Vec exposing (..) +import FileManager.Events exposing (..) + view : Model -> Html Msg view model = if model.open @@ -23,8 +28,15 @@ view model = if model.open [ class "fm-main" , onMouseMove (\_ -> None) ] - [ bar model.dir model.load - , files model + [ bar model + , div[class "files-container"] + [ filesTree model + , ( if model.viewMode == GridView then + filesGrid model + else + filesList model + ) + ] , div [ class "fm-control" ] [ button [ onClick <| EnvMsg Accept ] [ text "Close" ] ] @@ -39,10 +51,16 @@ view model = if model.open ] else div [] [] -bar : String -> Bool -> Html Msg -bar dir load = div [ class "fm-bar" ] - [ arrowIcon <| EnvMsg <| GetLs <| back dir - , div [ class "fm-text" ] [ text dir ] +bar : Model -> Html Msg +bar model = + let + dir = model.dir + load = model.load + filters = model.filters + in + div [ class "fm-bar" ] + [ arrowIcon dir <| EnvMsg <| GetLs (back dir) + , div [ class "fm-text" ] [ text (getDirPath dir) ] , if load then div [ class "fm-loader" ] [ svg [ width "25", height "25" ] @@ -50,20 +68,124 @@ bar dir load = div [ class "fm-bar" ] ] ] else div [] [] + , div [class "fm-bar-actions"] + [ div [class "btn-group"] + [ button [attribute "data-toggle" "dropdown", id "dropDownMenuSearch", type_ "button", class "btn btn-flat btn-sm dropdown-toggle"] + [ i [class "glyphicon glyphicon-search mr2"][] + ] + , ul [class "dropdown-menu search-dropdown pull-right"] + [ li[] + [ input [class "form-control", type_ "text", placeholder "Search...", onInput (\s -> UpdateFilters {filters | filter = s}), value filters.filter][]] + ] + ] + , ( if model.viewMode == GridView then + button [class "btn btn-flat btn-sm", onClick (ChangeViewMode ListView)] + [ i [class "glyphicon glyphicon-th-list"][] + ] + else + button [class "btn btn-flat btn-sm", onClick (ChangeViewMode GridView)] + [ i [class "glyphicon glyphicon-th-large" ][] + ] + ) + , div [class "btn-group"] + [ button [attribute "data-toggle" "dropdown", id "more", type_ "button", class "btn btn-flat btn-sm dropdown-toggle"] + [ i [class "glyphicon glyphicon-option-vertical"][] + ] + , mainContextMenu + ] + , button [ title "Close", class"btn btn-flat btn-sm", onClick <| EnvMsg Accept ] + [ i [class "fa fa-times"][] + ] + ] ] -files : Model -> Html Msg -files model = div - [ class "fm-files" +filesTree : Model -> Html Msg +filesTree model = + let + openedDir = model.filters.opened + + listFolders : String -> Html Msg + listFolders name = + case Dict.get name model.tree of + Nothing -> text "" + Just fs -> + ul[] + ( fs.childs + |> List.map (\f -> + let + parents = fs.parents + in + li [ ] + [ a [ onClick <| EnvMsg <| GetLsTree (List.append fs.parents [fs.name, f] ) ] + [ folderIcon 20 + , text f + ] + , if List.member f openedDir then listFolders f else text "" + ] + ) + ) + in + div [class "fm-filetree"] + [ listFolders "/" ] + +currentDir : List String -> String +currentDir dirList = case List.Extra.last dirList of + Just f -> f + Nothing -> "/" + +filesGrid : Model -> Html Msg +filesGrid model = + let + currentFiles = + model.files + |> List.filter (\f -> (filterSearch model.filters.filter (searchField f))) + |> List.sortBy .name + |> List.sortBy .type_ + |> indexedMap (renderFile model) + in + div + [ class "fm-files" + , class <| if model.drag then "fm-drag" else "" + , onMouseDown (\x y -> EnvMsg <| MouseDown Nothing x y) + , onMouseMove <| EnvMsg << MouseMove + , onMouseUp <| EnvMsg << MouseUp Nothing + , onDragEnter ShowDrop + ] + [ div [ class "fm-wrap" ] + [ div [ class "fm-fluid" ] + <| currentFiles + ++ reverse (map (renderUploading model.progress) (range 0 <| model.filesAmount - 1)) + ] + , if model.showDrop && model.hasWriteRights + then div [ class "fm-drop", onDragLeave HideDrop, onDrop GotFiles ] [] + else div [] [] + ] + +filesList : Model -> Html Msg +filesList model = + div + [ class "fm-files-list" , class <| if model.drag then "fm-drag" else "" , onMouseDown (\x y -> EnvMsg <| MouseDown Nothing x y) , onMouseMove <| EnvMsg << MouseMove , onMouseUp <| EnvMsg << MouseUp Nothing , onDragEnter ShowDrop ] - [ div [ class "fm-wrap" ] - [ div [ class "fm-fluid" ] - <| indexedMap (renderFile model) model.files + [ table [ class "dataTable" ] + [ thead[] + [ tr[class "head"] + [ th[class (thClass model.filters FileName ), onClick (UpdateFilters (sortTable model.filters FileName ))] [ text "Name" ] + , th[class (thClass model.filters FileSize ), onClick (UpdateFilters (sortTable model.filters FileSize ))] [ text "Size" ] + , th[class (thClass model.filters FileDate ), onClick (UpdateFilters (sortTable model.filters FileDate ))] [ text "Date" ] + , th[class (thClass model.filters FileRights ), onClick (UpdateFilters (sortTable model.filters FileRights ))] [ text "Permissions" ] + ] + ] + , tbody [] + <| ( model.files + |> List.filter (\f -> (filterSearch model.filters.filter (searchField f))) + |> List.sortWith (getSortFunction model) + |> indexedMap (renderFileList model) + ) ++ reverse (map (renderUploading model.progress) (range 0 <| model.filesAmount - 1)) ] , if model.showDrop && model.hasWriteRights @@ -71,22 +193,29 @@ files model = div else div [] [] ] -arrowIcon : Msg -> Html Msg -arrowIcon msg = button [ class "fm-arrow", onClick msg ] - [ - svg - [ attribute "height" "24" - , viewBox "0 0 24 24" - , attribute "width" "24" - , attribute "xmlns" "http://www.w3.org/2000/svg" - ] - [ path [ d "M0 0h24v24H0z", fill "none" ] [] - , path [ d "M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z", fill "#ffffff" ] [] +arrowIcon : List String -> Msg -> Html Msg +arrowIcon dir msg = + let + disable = List.length dir <= 1 + in + button [ class "fm-arrow", onClick msg , disabled disable] + [ svg + [ attribute "height" "24" + , viewBox "0 0 24 24" + , attribute "width" "24" + , attribute "xmlns" "http://www.w3.org/2000/svg" + ] + [ path [ d "M0 0h24v24H0z", fill "none" ] [] + , path [ d "M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z", fill "#ffffff" ] [] + ] ] - ] -back : String -> String -back route = "/" ++ (join "/" <| withDefault [] <| andThen tail <| tail <| reverse <| split "/" route) +back : List String -> String +back list = + case last list of + Just l -> l + Nothing -> "/" + renderUploading : Http.Progress -> Int -> Html Msg renderUploading progress i = div [ class "fm-file fm-upload" ] @@ -105,27 +234,50 @@ renderFile : Model -> Int -> FileMeta -> Html Msg renderFile { api, thumbnailsUrl, dir, selected, clipboardDir, clipboardFiles } i file = div [ id <| "fm-file-" ++ fromInt i, class <| "fm-file" ++ (if member file selected then " fm-selected" else "") - ++ (if dir == clipboardDir && member file clipboardFiles then " fm-cut" else "") + ++ (if (getDirPath dir) == clipboardDir && member file clipboardFiles then " fm-cut" else "") , title file.name , onMouseDown <| (\x y -> EnvMsg <| MouseDown (Just file) x y) , onMouseUp <| EnvMsg << (MouseUp <| Just file) - , onDoubleClick <| if file.type_ == "dir" then EnvMsg <| GetLs <| dir ++ file.name ++ "/" else Download + , onDoubleClick <| if file.type_ == "dir" then EnvMsg <| GetLs file.name else Download ] - [ renderThumb thumbnailsUrl api dir file + [ renderThumb thumbnailsUrl api (getDirPath dir) file , div [ class "fm-name" ] [ text file.name ] ] +renderFileList : Model -> Int -> FileMeta -> Html Msg +renderFileList { api, thumbnailsUrl, dir, selected, clipboardDir, clipboardFiles } i file = + tr + [ id <| "fm-file-" ++ fromInt i, class <| "fm-file-list" + ++ (if member file selected then " fm-selected" else "") + ++ (if (getDirPath dir) == clipboardDir && member file clipboardFiles then " fm-cut" else "") + , title file.name + , onMouseDown <| (\x y -> EnvMsg <| MouseDown (Just file) x y) + , onMouseUp <| EnvMsg << (MouseUp <| Just file) + , onDoubleClick <| if file.type_ == "dir" then EnvMsg <| (GetLs file.name) else Download + ] + [ td[] + [ div[] + [ renderThumb thumbnailsUrl api (getDirPath dir) file + , div [ class "fm-name" ] [ text file.name ] + ] + ] + , td[][text ((String.fromInt file.size) ++ " B")] + , td[][text file.date ] + , td[][text file.rights ] + ] + renderThumb : String -> String -> String -> FileMeta -> Html Msg renderThumb thumbApi api dir file = if file.type_ == "dir" - then div [ class "fm-thumb" ] [ fileIcon ] + then div [ class "fm-thumb" ] [ (folderIcon 48) ] else renderFileThumb api thumbApi <| dir ++ file.name fileIcon : Html Msg -fileIcon = svg [ attribute "height" "48", viewBox "0 0 24 24", attribute "width" "48", attribute "xmlns" "http://www.w3.org/2000/svg" ] - [ path [ d "M10 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z", fill "#ffb900" ] - [] +fileIcon = + svg [ attribute "height" "48", viewBox "0 0 24 24", attribute "width" "48", attribute "xmlns" "http://www.w3.org/2000/svg" ] + [ path [ d "M6 2c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6H6zm7 7V3.5L18.5 9H13z", fill "#0078d4" ] + [] , path [ d "M0 0h24v24H0z", fill "none" ] - [] + [] ] renderFileThumb : String -> String -> String -> Html Msg @@ -133,15 +285,19 @@ renderFileThumb api thumbApi fullName = if member (getExt fullName) ["jpg", "jpe then div [ class "fm-thumb" ] [ img [ src <| thumbApi ++ fullName, draggable "false" ] [] ] - else div [ class "fm-thumb" ] [ folderIcon ] + else div [ class "fm-thumb" ] [ fileIcon ] -folderIcon : Html Msg -folderIcon = svg [ attribute "height" "48", viewBox "0 0 24 24", attribute "width" "48", attribute "xmlns" "http://www.w3.org/2000/svg" ] - [ path [ d "M6 2c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6H6zm7 7V3.5L18.5 9H13z", fill "#0078d4" ] +folderIcon : Int -> Html Msg +folderIcon s = + let + size = String.fromInt s + in + svg [ attribute "height" size, viewBox "0 0 24 24", attribute "width" size, attribute "xmlns" "http://www.w3.org/2000/svg" ] + [ path [ d "M10 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z", fill "#ffb900" ] [] - , path [ d "M0 0h24v24H0z", fill "none" ] + , path [ d "M0 0h24v24H0z", fill "none" ] [] - ] + ] getExt : String -> String getExt name = withDefault "" <| head <| reverse <| split "." name @@ -179,7 +335,7 @@ contextMenu (Vec2 x y) maybe paste many filesAmount = if filesAmount > 0 , (if many then text "" else button [ class "div white", onClick (OpenNameDialog (Rename file file.name)) ] [ text "Rename" ]) , button [ class "div white", onClick Cut ] [ text "Cut" ] , (if paste && file.type_ == "dir" then button [ class "div white", onClick Paste ] [ text "Paste" ] else text "") - , button [ class "div white", onClick Delete ] [ text "Delete" ] + , button [ class "div white text-danger", onClick Delete ] [ text "Delete" ] ] Nothing -> [ button [ class "div white", onClick ChooseFiles ] [ text "Upload" ] @@ -188,6 +344,23 @@ contextMenu (Vec2 x y) maybe paste many filesAmount = if filesAmount > 0 , (if paste then button [ class "div white", onClick Paste ] [ text "Paste" ] else text "" ) ] +mainContextMenu : Html Msg +mainContextMenu = + ul[class "dropdown-menu pull-right"] + [ li[] + [ a [ onClick (OpenNameDialog (NewDir "")) ] + [ i [class "glyphicon glyphicon-plus"][] + , text "New folder" + ] + ] + , li[] + [ a [ onClick ChooseFiles ] + [ i [class "glyphicon glyphicon-cloud-upload"][] + , text "Upload files" + ] + ] + ] + nameDialog : DialogAction -> Html Msg nameDialog dialogState = let @@ -221,11 +394,70 @@ nameDialog dialogState = [ label [] [ strong [] [ text "Name" ] , br [] [] - , input [ type_ "text", value n, onInput Name ] [] + , input [ type_ "text", class "form-control", value n, onInput Name ] [] ] , div [] - [ button [ class "fm-button", onClick CloseNameDialog ] [ text "Cancel" ] - , button [ class "fm-button", onClick ConfirmNameDialog ] [ text "Ok" ] + [ button [ class "fm-button btn btn-default" , onClick CloseNameDialog ] [ text "Cancel" ] + , button [ class "fm-button btn btn-success" , onClick ConfirmNameDialog ] [ text "Confirm" ] ] ] ] + +searchString : String -> String +searchString str = str + |> String.toLower + |> String.trim + +filterSearch : String -> List String -> Bool +filterSearch filterString searchFields = + let + -- Join all the fields into one string to simplify the search + stringToCheck = searchFields + |> String.join "|" + |> String.toLower + in + String.contains (searchString filterString) stringToCheck + +thClass : Filters -> SortBy -> String +thClass filters sortBy = + if sortBy == filters.sortBy then + case filters.sortOrder of + Asc -> "sorting_asc" + Desc -> "sorting_desc" + else + "sorting" + +sortTable : Filters -> SortBy -> Filters +sortTable filters sortBy = + let + order = + case filters.sortOrder of + Asc -> Desc + Desc -> Asc + in + if sortBy == filters.sortBy then + { filters | sortOrder = order} + else + { filters | sortBy = sortBy, sortOrder = Asc} + +getSortFunction : Model -> FileMeta -> FileMeta -> Order +getSortFunction model f1 f2 = + let + order = case model.filters.sortBy of + FileName -> N.compare f1.name f2.name + FileSize -> Basics.compare f1.size f2.size + FileDate -> N.compare f1.date f2.date + FileRights -> N.compare f1.rights f2.rights + in + if model.filters.sortOrder == Asc then + order + else + case order of + LT -> GT + EQ -> EQ + GT -> LT + +searchField : FileMeta -> List String +searchField file = + [ file.name + ] \ No newline at end of file diff --git a/webapp/sources/rudder/rudder-web/src/main/style/rudder/rudder-filemanager.css b/webapp/sources/rudder/rudder-web/src/main/style/rudder/rudder-filemanager.css index 89c6aceecc8..f46a3e8a926 100644 --- a/webapp/sources/rudder/rudder-web/src/main/style/rudder/rudder-filemanager.css +++ b/webapp/sources/rudder/rudder-web/src/main/style/rudder/rudder-filemanager.css @@ -19,6 +19,10 @@ cursor: pointer; } +.fm-modal .fm-button{ + min-width: 70px; +} + .fm-simple-screen { position: fixed; left: 0; @@ -26,7 +30,6 @@ width: 100%; height: 100%; z-index: 10000; - background-color: rgba(0, 0, 0, 0.75); } .fm-main { @@ -39,12 +42,14 @@ right: 100px; z-index: 1; font-family: var(--font-sans); + font-size: 14px; } .fm-bar { display: flex; align-items: center; - background-color: #0078D4; + background-color: #F8F9FC; + border-bottom: 1px solid #D6DEEF; padding: 0 12px; } @@ -53,6 +58,11 @@ border: none; background: none; cursor: pointer; + display: inline-flex; +} + +.fm-arrow > svg > path:last-child{ + fill: #738195; } .fm-arrow:focus { @@ -62,7 +72,8 @@ .fm-text { flex-grow: 1; margin-left: 10px; - color: #FFFFFF; + color: #738195; + font-weight: 500; } .fm-bar circle { @@ -93,6 +104,7 @@ border-bottom: 1px solid #FFFFFF; background-color: #FFFFFF; overflow: hidden; + flex: 1; } .fm-wrap { @@ -127,11 +139,11 @@ } .fm-file:hover { - background-color: #DEECF9; + background-color: #eef1f9; } .fm-selected { - background-color: #C7E0F4; + background-color: #C7E0F4 !important; } .fm-drag .fm-selected, .fm-cut { @@ -143,7 +155,7 @@ justify-content: center; align-items: center; width: 100px; - height: 100px; + height: 60px; } .fm-upload .fm-thumb { @@ -166,9 +178,7 @@ .fm-name { width: 100px; - margin-top: 10px; overflow: hidden; - font-size: 0.8em; text-align: center; } @@ -177,17 +187,18 @@ justify-content: flex-end; align-items: center; padding: 0 12px; - background-color: #0078D4; + background-color: #F8F9FC; + border-top: 1px solid #D6DEEF; } .fm-control button { margin-left: 7px; padding: 5px 10px; - border: none; - border-radius: 5px; - color: #0078D4; + border-radius: 4px; + color: #041922; background-color: #FFFFFF; - cursor: pointer; + cursor: pointer; + border: 1px solid #D6DEEF; } .fm-helper { @@ -215,8 +226,9 @@ position: fixed; width: 150px; padding: 5px 0; - border: 1px solid #D0D0D0; + border: 1px solid #D6DEEF; background-color: #FFFFFF; + border-radius: 4px; } .fm-context-menu button { @@ -230,7 +242,7 @@ } .fm-context-menu button:hover { - background-color: #D0D0D0; + background-color: #eef1f9; } .fm-context-menu .disabled { @@ -270,17 +282,23 @@ z-index: 1; width: 100%; height: 100%; - background-color: rgba(0, 0, 0, 0.75); + background-color: #041922AA; } .fm-modal { padding: 20px; background-color: #FFFFFF; + border-radius: 4px; } .fm-modal label { display: block; - margin-bottom: 10px; + margin-bottom: 14px; + min-width: 400px; +} + +.fm-modal label > strong{ + margin-bottom: 6px; } .fm-modal > h1 { @@ -300,4 +318,104 @@ .fm-modal textarea { width: 50vw; height: 50vh; -} \ No newline at end of file +} + +/* BAR BUTTONS */ +.fm-bar-actions > .btn, +.fm-bar-actions > .btn-group{ + margin-left: 6px; +} + +.fm-bar-actions .btn{ + font-size: 16px; + background-color: transparent !important; + color: #738195; +} + +/* SEARCH FILTER */ +.dropdown-menu.search-dropdown{ + padding: 10px; + background-color: #F8F9FC; + width: 250px; +} + +/* FILES TREE */ +.files-container { + background-color: #fff; + display: flex; + flex-direction: row; +} + +.fm-filetree { + width: 320px; + height: 100%; + background-color: #fafafa; + border-right: 1px solid #D6DEEF; +} + +.fm-filetree ul{ + padding-left: 24px; + position: relative; +} + +.fm-filetree > ul{ + padding-left: 15px; +} + +.fm-filetree ul li a{ + display: flex; + align-items: center; + padding: 6px 0; + cursor: pointer; + color: #738195; + position: relative; + padding-left: 24px; +} + +.fm-filetree ul li a:hover{ + background-color: #eef1f9; + color: #041922; +} +.fm-filetree ul li a:before, +.fm-filetree ul ul:before{ + content: ""; + border-left: 2px solid #D6DEEF; + display: block; + position: absolute; + height: 100%; + left: 10px; + top: 0; +} +.fm-filetree ul > li:last-child a:before { + height: 50%; +} +.fm-filetree ul > li:last-child ul:before { + content: initial; +} +.fm-filetree ul li svg{ + position: absolute; + left: 0; +} + +.fm-files-list{ + width: 100%; +} +.fm-files-list > .dataTable { + border: none; +} + +tr.fm-file-list > td > div { + display: flex; + align-items: center; +} + +tr.fm-file-list .fm-thumb { + width: 30px; + height: 30px; + margin-right: 4px; +} + +tr.fm-file-list .fm-name { + margin-left: 4px; + width: initial; +} diff --git a/webapp/sources/rudder/rudder-web/src/main/style/rudder/rudder-main.css b/webapp/sources/rudder/rudder-web/src/main/style/rudder/rudder-main.css index 6ee32b75f0c..0029d2f1240 100644 --- a/webapp/sources/rudder/rudder-web/src/main/style/rudder/rudder-main.css +++ b/webapp/sources/rudder/rudder-web/src/main/style/rudder/rudder-main.css @@ -193,6 +193,7 @@ body > .modal-backdrop.fade.in{ border: 1px solid #d6deef; -webkit-box-shadow: none; box-shadow: none; + font-weight: normal; } .content-wrapper .form-control:active:focus { border-color: #d6deef; diff --git a/webapp/sources/rudder/rudder-web/src/main/style/rudder/rudder-technique-editor.css b/webapp/sources/rudder/rudder-web/src/main/style/rudder/rudder-technique-editor.css index f23073406a4..f799cb9672a 100644 --- a/webapp/sources/rudder/rudder-web/src/main/style/rudder/rudder-technique-editor.css +++ b/webapp/sources/rudder/rudder-web/src/main/style/rudder/rudder-technique-editor.css @@ -63,7 +63,7 @@ max-height: 250px; overflow: auto; } -ul.dropdown-menu:not(.dropdown-search) { +ul.dropdown-menu:not(.search-dropdown) { padding: 0; min-width: 170px; margin-left: -75px; diff --git a/webapp/sources/rudder/rudder-web/src/main/style/rudder/rudder-template.css b/webapp/sources/rudder/rudder-web/src/main/style/rudder/rudder-template.css index 247541f22b3..eca835a4510 100644 --- a/webapp/sources/rudder/rudder-web/src/main/style/rudder/rudder-template.css +++ b/webapp/sources/rudder/rudder-web/src/main/style/rudder/rudder-template.css @@ -725,10 +725,12 @@ .rudder-template .template-main .dataTable tr.details td.details{ padding-left: 0; } -.rudder-template .dataTable > tbody > tr:nth-child(even){ +.rudder-template .dataTable > tbody > tr:nth-child(even), +.filemanager-container .dataTable > tbody > tr:nth-child(even){ background-color: #F8F9FC; } -.rudder-template .dataTable > tbody > tr:nth-child(even):hover{ +.rudder-template .dataTable > tbody > tr:nth-child(even):hover, +.filemanager-container .dataTable > tbody > tr:nth-child(even):hover{ background-color: #eef1f9; } .rudder-template .template-main .main-table > .table-container > .dataTable > tbody > tr.is-expired {