Эх сурвалжийг харах

Merge pull request #2268 from serghey-rodin/feature/r-1.0.0.7

Release UI 1.0.0.7
Anton Reutov 3 жил өмнө
parent
commit
94d60267a8
31 өөрчлөгдсөн 473 нэмэгдсэн , 274 устгасан
  1. 3 3
      src/react/package-lock.json
  2. 1 1
      src/react/package.json
  3. 39 38
      src/react/src/ControlPanelService/Db.js
  4. 3 7
      src/react/src/components/Hotkeys/Hotkeys.jsx
  5. 83 48
      src/react/src/components/Lists/DirectoryList/DirectoryList.jsx
  6. 9 1
      src/react/src/components/Lists/Row/Row.jsx
  7. 22 20
      src/react/src/components/Lists/Row/Row.scss
  8. 0 1
      src/react/src/components/MainNav/Panel/Notifications/Notifications.jsx
  9. 1 1
      src/react/src/components/MainNav/Toolbar/Toolbar.scss
  10. 170 83
      src/react/src/components/Menu/Menu.jsx
  11. 23 4
      src/react/src/components/Modal/AddDirectory.jsx
  12. 23 4
      src/react/src/components/Modal/AddFile.jsx
  13. 47 50
      src/react/src/components/Modal/Modal.jsx
  14. 24 9
      src/react/src/components/Modal/Modal.scss
  15. 1 1
      src/react/src/components/Preview/Preview.jsx
  16. 12 2
      src/react/src/components/Server/Server.scss
  17. 11 0
      src/react/src/containers/App/App.scss
  18. 0 0
      web/static/css/main.55ab5a88.chunk.css
  19. 0 0
      web/static/css/main.55ab5a88.chunk.css.map
  20. 0 0
      web/static/css/main.9f0c683e.chunk.css
  21. 0 0
      web/static/css/main.9f0c683e.chunk.css.map
  22. 0 0
      web/static/index.html
  23. 0 1
      web/static/js/2.5dc90ea3.chunk.js
  24. 0 0
      web/static/js/2.5dc90ea3.chunk.js.map
  25. 1 0
      web/static/js/2.7cb4195c.chunk.js
  26. 0 0
      web/static/js/2.7cb4195c.chunk.js.LICENSE.txt
  27. 0 0
      web/static/js/2.7cb4195c.chunk.js.map
  28. 0 0
      web/static/js/main.57f35a42.chunk.js
  29. 0 0
      web/static/js/main.57f35a42.chunk.js.map
  30. 0 0
      web/static/js/main.a9be926e.chunk.js
  31. 0 0
      web/static/js/main.a9be926e.chunk.js.map

+ 3 - 3
src/react/package-lock.json

@@ -4396,9 +4396,9 @@
       }
     },
     "dayjs": {
-      "version": "1.10.7",
-      "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.10.7.tgz",
-      "integrity": "sha512-P6twpd70BcPK34K26uJ1KT3wlhpuOAPoMwJzpsIWUxHZ7wpmbdZL/hQqBDfz7hGurYSa5PhzdhDHtt319hL3ig=="
+      "version": "1.11.4",
+      "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.4.tgz",
+      "integrity": "sha512-Zj/lPM5hOvQ1Bf7uAvewDaUcsJoI6JmNqmHhHl3nyumwe0XHwt8sWdOVAPACJzCebL8gQCi+K49w7iKWnGwX9g=="
     },
     "debug": {
       "version": "3.1.0",

+ 1 - 1
src/react/package.json

@@ -13,7 +13,7 @@
     "axios": "^0.21.4",
     "bootstrap": "^4.3.1",
     "classname": "0.0.0",
-    "dayjs": "^1.10.7",
+    "dayjs": "^1.11.4",
     "jquery": "^3.5.1",
     "node-sass": "^4.14.1",
     "perfect-scrollbar": "^1.5.3",

+ 39 - 38
src/react/src/ControlPanelService/Db.js

@@ -1,49 +1,49 @@
-import axios from "axios";
-import { getAuthToken } from "src/utils/token";
+import axios from 'axios'
+import { getAuthToken } from 'src/utils/token'
 
-const BASE_URL = window.location.origin;
-const webApiUri = '/api/v1/list/db/index.php';
-const addDbApiUri = '/api/v1/add/db/index.php';
-const optionalDbInfoUri = '/api/v1/add/db/index.php';
-const dbInfoUri = '/api/v1/edit/db/index.php';
-const updateDatabaseUri = '/api/v1/edit/db/index.php';
+const BASE_URL = window.location.origin
+const webApiUri = '/api/v1/list/db/index.php'
+const addDbApiUri = '/api/v1/add/db/index.php'
+const optionalDbInfoUri = '/api/v1/add/db/index.php'
+const dbInfoUri = '/api/v1/edit/db/index.php'
+const updateDatabaseUri = '/api/v1/edit/db/index.php'
 
 export const getDatabaseList = () => {
-  return axios.get(BASE_URL + webApiUri);
+  return axios.get(BASE_URL + webApiUri)
 }
 
 export const bulkAction = (action, domainNameSystems) => {
-  const formData = new FormData();
-  formData.append("action", action);
-  formData.append("token", getAuthToken());
+  const formData = new FormData()
+  formData.append('action', action)
+  formData.append('token', getAuthToken())
 
-  domainNameSystems.forEach(domainNameSystem => {
-    formData.append("database[]", domainNameSystem);
-  });
+  domainNameSystems.forEach((domainNameSystem) => {
+    formData.append('database[]', domainNameSystem)
+  })
 
-  return axios.post(BASE_URL + '/api/v1/bulk/db/', formData);
-};
+  return axios.post(BASE_URL + '/api/v1/bulk/db/', formData)
+}
 
-export const handleAction = uri => {
+export const handleAction = (uri) => {
   return axios.get(BASE_URL + uri, {
     params: {
-      token: getAuthToken()
-    }
-  });
+      token: getAuthToken(),
+    },
+  })
 }
 
 export const getDbOptionalInfo = () => {
-  return axios.get(BASE_URL + optionalDbInfoUri);
+  return axios.get(BASE_URL + optionalDbInfoUri)
 }
 
-export const addDatabase = data => {
-  let formDataObject = new FormData();
+export const addDatabase = (data) => {
+  let formDataObject = new FormData()
 
   for (let key in data) {
-    formDataObject.append(key, data[key]);
+    formDataObject.append(key, data[key])
   }
 
-  return axios.post(BASE_URL + addDbApiUri, formDataObject);
+  return axios.post(BASE_URL + addDbApiUri, formDataObject)
 }
 
 export const dbCharsets = [
@@ -69,6 +69,7 @@ export const dbCharsets = [
   'latin5',
   'armscii8',
   'utf8',
+  'utf8mb4',
   'ucs2',
   'cp866',
   'keybcs2',
@@ -82,29 +83,29 @@ export const dbCharsets = [
   'binary',
   'geostd8',
   'cp932',
-  'eucjpms'
-];
+  'eucjpms',
+]
 
-export const getDatabaseInfo = database => {
+export const getDatabaseInfo = (database) => {
   return axios.get(BASE_URL + dbInfoUri, {
     params: {
       database,
-      token: getAuthToken()
-    }
-  });
+      token: getAuthToken(),
+    },
+  })
 }
 
 export const updateDatabase = (data, database) => {
-  let formDataObject = new FormData();
+  let formDataObject = new FormData()
 
   for (let key in data) {
-    formDataObject.append(key, data[key]);
+    formDataObject.append(key, data[key])
   }
 
   return axios.post(BASE_URL + updateDatabaseUri, formDataObject, {
     params: {
       database,
-      token: getAuthToken()
-    }
-  });
-}
+      token: getAuthToken(),
+    },
+  })
+}

+ 3 - 7
src/react/src/components/Hotkeys/Hotkeys.jsx

@@ -34,10 +34,10 @@ const Hotkeys = props => {
           </li>
           <li>
             <span className="name">n</span>
-            <span className="description">{i18n['New Fille']}</span>
+            <span className="description">{i18n['New File']}</span>
           </li>
           <li>
-            <span className="name">F7</span>
+            <span className="name">F6</span>
             <span className="description">{i18n['New Folder']}</span>
           </li>
           <li>
@@ -56,10 +56,6 @@ const Hotkeys = props => {
             <span className="name">F5</span>
             <span className="description">{i18n['Copy']}</span>
           </li>
-          <li>
-            <span className="name">F5</span>
-            <span className="description">{i18n['Copy']}</span>
-          </li>
           <li>
             <span className="name">F8 / Del</span>
             <span className="description">{i18n['Delete']}</span>
@@ -107,7 +103,7 @@ const Hotkeys = props => {
             <span className="description">{i18n['Open File / Enter Directory']}</span>
           </li>
           <li>
-            <span className="name">F4</span>
+            <span className="name">F3</span>
             <span className="description">{i18n['Edit File']}</span>
           </li>
           <li>

+ 83 - 48
src/react/src/components/Lists/DirectoryList/DirectoryList.jsx

@@ -6,6 +6,17 @@ import Row from '../Row/Row';
 import '../List.scss';
 
 class DirectoryList extends Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      orderType: "descending",
+      sortingType: "Type",
+      itemsSelected: [],
+      listingItems: [],
+      cursor: 0
+    };
+  }
+
   static propTypes = {
     changePathAfterToggle: PropTypes.func,
     openCertainDirectory: PropTypes.func,
@@ -27,13 +38,6 @@ class DirectoryList extends Component {
     data: PropTypes.array
   }
 
-  state = {
-    orderType: "descending",
-    sortingType: "Type",
-    itemsSelected: [],
-    cursor: 0
-  };
-
   UNSAFE_componentWillMount = () => {
     if (localStorage.getItem(`${this.props.list}Sorting`) && localStorage.getItem(`${this.props.list}Order`)) {
       this.setState({ sortingType: localStorage.getItem(`${this.props.list}Sorting`), orderType: localStorage.getItem(`${this.props.list}Order`) });
@@ -117,20 +121,21 @@ class DirectoryList extends Component {
   }
 
   handleLiSelection = (e) => {
-    const { data, isActive, modalVisible, changePath, path } = this.props;
+    const { isActive, modalVisible, changePath, path } = this.props;
     const { cursor } = this.state;
+    const { listing } = this.getDataBySortingType()
 
     if (!isActive || modalVisible) {
       return;
     }
 
     if (e.keyCode === 40) {
-      if (cursor === data.listing.length - 1) {
+      if (cursor === listing.length - 1) {
         return;
       }
 
-      if (e.shiftKey) {
-        let name = data.listing[cursor].name;
+      if (e.shiftKey) { 
+        let name = listing[cursor].name;
         this.addToSelection(name);
       }
 
@@ -145,7 +150,7 @@ class DirectoryList extends Component {
       }
 
       if (e.shiftKey) {
-        let name = data.listing[cursor].name;
+        let name = listing[cursor - 1].name;
         this.addToSelection(name);
       }
 
@@ -160,9 +165,15 @@ class DirectoryList extends Component {
   }
 
   passData = () => {
-    const { data, passData } = this.props;
-    const { name, permissions, type } = data.listing[this.state.cursor];
-    passData(this.state.cursor, name, permissions, type);
+    const { passData: passDataToParent } = this.props;
+    const { firstItem, listing } =  this.getDataBySortingType()
+    if (this.state.cursor === 0) {
+      const { name, permissions, type } = firstItem;
+      passDataToParent(this.state.cursor, name, permissions, type);
+    } else {
+      const { name, permissions, type } = listing[this.state.cursor - 1];
+      passDataToParent(this.state.cursor, name, permissions, type);
+    }
   }
 
   openDirectory = (name) => {
@@ -231,50 +242,74 @@ class DirectoryList extends Component {
   sortData = (a, b) => {
     switch (this.state.sortingType) {
       case "Type": return this.sortByType(a, b);
-      case "Size": if (a.type !== "d" && b.type !== "d") { return this.sortBySize(a, b) }; break;
+      case "Size": return this.sortBySize(a, b);
       case "Date": return this.sortByDate(a, b);
       case "Name": return this.sortByName(a, b);
       default: return this.sortByType(a, b);
     }
   }
 
+  getDataBySortingType = () => {
+    let firstItem, listing = [];
+    this.props.data.listing.forEach(item => {
+      if (item.name === '' && item.type === 'd') {
+        firstItem = item
+      } else {
+        listing.push(item)
+      }
+    })
+    if (this.state.sortingType !== 'Type') {
+      listing = [
+        ...listing.filter(item => item.type === 'd').sort((a, b) => this.sortByName(a, b)),
+        ...listing.filter(item => item.type === 'f').sort((a, b) => this.sortData(a, b))
+      ]
+    } else {
+      listing = listing.sort((a, b) => this.sortData(a, b))
+    }
+    return { firstItem, listing }
+  }
+
   rows = () => {
     const { isActive, modalVisible, path, download } = this.props;
     const { cursor } = this.state;
-    const data = { ...this.props.data };
+    const { listing, firstItem } = this.getDataBySortingType()
 
-    if (data.listing.length !== 0) {
-      let sortedData = data.listing.sort((a, b) => this.sortData(a, b));
+    if (listing.length || firstItem) {
       return (
-        sortedData.map((item, key) =>
-          (item.name !== "" && sortedData.length !== 0) ?
-            (<Row key={key}
-              selectOnClick={(cursor, name, permissions, type) => {
-                this.setState({ cursor });
-                this.props.passData(cursor, name, permissions, type);
-              }}
-              selectMultiple={() => this.addToSelection(item.name)}
-              selected={this.isSelected(item.name)}
-              openDirectory={this.openDirectory}
-              modalVisible={modalVisible}
-              activeRow={key === cursor}
-              isActiveList={isActive}
-              download={download}
-              cursor={key}
-              data={item}
-              path={path} />) :
-            (<Row key={key}
-              selectOnClick={(cursor, name, permissions, type) => {
-                this.setState({ cursor });
-                this.props.passData(cursor, name, permissions, type);
-              }}
-              openDirectory={this.moveBack}
-              modalVisible={modalVisible}
-              activeRow={key === cursor}
-              isActiveList={isActive}
-              cursor={key}
-              data={item}
-              path={path} />))
+        <>
+          <Row
+            selectOnClick={(cursor, name, permissions, type) => {
+              this.setState({ cursor });
+              this.props.passData(cursor, name, permissions, type);
+            }}
+            openDirectory={this.moveBack}
+            modalVisible={modalVisible}
+            activeRow={0 === cursor}
+            isActiveList={isActive}
+            cursor={0}
+            data={firstItem}
+            path={path} />
+          {
+            listing.map((item, key) => (
+              <Row
+                key={key + 1}
+                selectOnClick={(cursor, name, permissions, type) => {
+                  this.setState({ cursor });
+                  this.props.passData(cursor, name, permissions, type);
+                }}
+                selectMultiple={() => this.addToSelection(item.name)}
+                selected={this.isSelected(item.name)}
+                openDirectory={this.openDirectory}
+                modalVisible={modalVisible}
+                activeRow={key + 1 === cursor}
+                isActiveList={isActive}
+                download={download}
+                cursor={key + 1}
+                data={item}
+                path={path} />
+            ))
+          }
+        </>
       );
     }
   }

+ 9 - 1
src/react/src/components/Lists/Row/Row.jsx

@@ -5,6 +5,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { faJs, faCss3, faPhp, faHtml5, faSass } from '@fortawesome/free-brands-svg-icons';
 import './Row.scss';
 import { connect } from 'react-redux';
+import dayjs from 'dayjs'
 
 class Row extends Component {
   static propTypes = {
@@ -127,6 +128,13 @@ class Row extends Component {
     return (<span className="date">{getMonth} {getDay}</span>);
   }
 
+  timeFormatter = (date = new Date(), time) => {
+    const year = dayjs(date).year()
+    const currentYear = dayjs().year()
+    if (year === currentYear) return time
+    return year
+  }
+
   glyph = () => {
     const { data: { type, name } } = this.props;
 
@@ -182,7 +190,7 @@ class Row extends Component {
         <span className="fOwner">{owner}</span>
         <span className="fSize">{this.sizeFormatter(size)}</span>
         <span className="fDate">{this.dateFormatter(date)}</span>
-        <span className="fTime">{time}</span>
+        <span className="fTime">{this.timeFormatter(date, time)}</span>
       </li>
     );
   }

+ 22 - 20
src/react/src/components/Lists/Row/Row.scss

@@ -87,11 +87,11 @@
     margin-left: 5px;
     line-height: 34px;
     font-size: 15px;
-    white-space: nowrap; 
-    transition: all ease-out .3s;
-  
+    white-space: nowrap;
+    transition: all ease-out 0.3s;
+
     &:hover {
-      transition: all ease-out .2s;
+      transition: all ease-out 0.2s;
       background: rgb(201, 199, 199);
       border-radius: 4px;
       cursor: pointer;
@@ -100,7 +100,7 @@
 }
 
 .list .list-container ul li.active .fName .name:hover {
-  background: #F0B607;
+  background: #f0b607;
   color: black;
 }
 
@@ -160,20 +160,22 @@ li.inactive {
   background: rgb(220, 220, 220);
 }
 
-@media (max-width: 1400px){
-  .fPermissions, .fOwner {
-    display: none;
-  }
-}
-
-@media (max-width: 1100px){
-  .fDate {
-    display: none;
-  }
-}
-
-@media (max-width: 970px){
-  .fTime {
-    display: none;
+@media (max-width: 1320px) {
+  .list .list-container ul li {
+    .fName {
+      width: 210px;
+    }
+    .fSize {
+      width: 75px;
+    }
+    .fDate {
+      width: 40px;
+    }
+    .fTime {
+      width: 50px;
+    }
+    .fPermissions {
+      margin: 0;
+    }
   }
 }

+ 0 - 1
src/react/src/components/MainNav/Panel/Notifications/Notifications.jsx

@@ -15,7 +15,6 @@ const Notifications = () => {
 
   useEffect(() => {
     if (!notifications.length) {
-      console.log(notifications);
       fetchData();
     }
   }, [notifications]);

+ 1 - 1
src/react/src/components/MainNav/Toolbar/Toolbar.scss

@@ -151,7 +151,7 @@
   box-shadow: rgba(200, 200, 200, 0.5) 0px 5px 3px 0px;
 }
 
-@media (max-width: 1350px) {
+@media (max-width: 1390px) {
   .toolbar {
     padding: 3px 10% 1px;
   }

+ 170 - 83
src/react/src/components/Menu/Menu.jsx

@@ -1,148 +1,185 @@
-import React, { createRef, useEffect } from 'react';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import './Menu.scss';
-import { useSelector } from 'react-redux';
-import { Link } from 'react-router-dom';
+import React, { useCallback, useEffect, useRef } from 'react'
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import './Menu.scss'
+import { useSelector } from 'react-redux'
+import { Link } from 'react-router-dom'
 
 const Menu = (props) => {
-  const { i18n } = useSelector(state => state.session);
-  const inputFile = createRef();
+  const { i18n } = useSelector((state) => state.session)
+  const inputFile = useRef()
 
-  useEffect(() => {
-    document.addEventListener("keydown", hotKeys);
+  const handleUserKeyDown = useCallback((event) => hotKeys(event), [props])
 
-    return () => document.removeEventListener("keydown", hotKeys);
-  }, []);
+  useEffect(() => {
+    document.addEventListener('keydown', handleUserKeyDown)
+    return () => document.removeEventListener('keydown', handleUserKeyDown)
+  }, [handleUserKeyDown])
 
   const newFile = () => {
-    props.openModal("Add file");
+    props.openModal('Add file')
   }
 
   const newDirectory = () => {
-    props.openModal("Add directory");
+    props.openModal('Add directory')
   }
 
   const deleteFile = () => {
-    const { selection, openModal, cursor } = props;
+    const { selection, openModal, cursor } = props
     if (selection.length === 0) {
       if (cursor === 0) {
-        openModal("Nothing selected");
+        openModal('Nothing selected')
       } else {
-        openModal("Delete");
+        openModal('Delete')
       }
     } else {
-      openModal("Delete", selection.length);
+      openModal('Delete', selection.length)
     }
   }
 
   const rename = () => {
+    console.log(props)
     if (props.cursor === 0) {
-      props.openModal("Nothing selected");
+      props.openModal('Nothing selected')
     } else {
-      props.openModal("Rename");
+      props.openModal('Rename')
     }
   }
 
   const permissions = () => {
     if (props.cursor === 0) {
-      props.openModal("Nothing selected");
+      props.openModal('Nothing selected')
     } else {
-      props.openModal("Permissions");
+      props.openModal('Permissions')
     }
   }
 
   const move = () => {
-    const { selection, openModal, cursor } = props;
+    const { selection, openModal, cursor } = props
     if (selection.length === 0) {
       if (cursor === 0) {
-        openModal("Nothing selected");
+        openModal('Nothing selected')
       } else {
-        openModal("Move");
+        openModal('Move')
       }
     } else {
-      openModal("Move", selection.length);
+      openModal('Move', selection.length)
     }
   }
 
   const archive = () => {
-    const { selection, openModal, cursor } = props;
+    const { selection, openModal, cursor } = props
 
     if (selection.length === 0) {
       if (cursor === 0) {
-        openModal("Nothing selected");
+        openModal('Nothing selected')
       } else {
-        openModal("Archive");
+        openModal('Archive')
       }
     } else {
-      openModal("Archive", selection.length);
+      openModal('Archive', selection.length)
     }
   }
 
   const extract = () => {
     if (props.cursor === 0) {
-      props.openModal("Nothing selected");
+      props.openModal('Nothing selected')
     } else {
-      props.openModal("Extract");
+      props.openModal('Extract')
     }
   }
 
   const copy = () => {
-    const { selection, openModal, cursor } = props;
+    const { selection, openModal, cursor } = props
     if (selection.length === 0) {
       if (cursor === 0) {
-        openModal("Nothing selected");
+        openModal('Nothing selected')
       } else {
-        openModal("Copy");
+        openModal('Copy')
       }
     } else {
-      openModal("Copy", selection.length);
+      openModal('Copy', selection.length)
     }
   }
 
   const upload = (e) => {
     if (e.target.files.length === 0) {
-      return;
+      return
     }
 
-    props.upload(e.target.files);
+    props.upload(e.target.files)
   }
 
   const download = () => {
     if (props.cursor === 0) {
-      props.openModal("Nothing selected");
-    } else if (props.itemType === "d") {
-      props.openModal("Nothing selected", null, true);
+      props.openModal('Nothing selected')
+    } else if (props.itemType === 'd') {
+      props.openModal('Nothing selected', null, true)
     } else {
-      props.download();
+      props.download()
     }
   }
 
   const hotKeys = (e) => {
-    let isSearchInputFocused = document.querySelector('input:focus') || document.querySelector('textarea:focus');
-
-    if (props.modalVisible || isSearchInputFocused) return;
-
-    if (e.shiftKey && e.keyCode === 117) {
-      rename();
+    e.stopPropagation()
+    let isSearchInputFocused = document.querySelector('input:focus') || document.querySelector('textarea:focus')
+    if (props.modalVisible || isSearchInputFocused) return
+    if (e.shiftKey && e.keyCode === 118) {
+      e.preventDefault()
+      rename()
+      return
     }
 
     switch (e.keyCode) {
-      case 46: return deleteFile();
-      case 65: return archive();
-      case 68: return download();
-      case 77: return move();
-      case 78: return newFile();
-      case 85: return inputFile.click();
-      case 113: return rename();
-      case 115: return permissions();
-      case 116: return copy();
-      case 118: return newDirectory();
-      case 119: return deleteFile();
-      default: break;
+      // u
+      case 85:
+        e.preventDefault();
+        return inputFile.current.click()
+      // n
+      case 78:
+        e.preventDefault()
+        return newFile()
+      // F6
+      case 118:
+        e.preventDefault()
+        return newDirectory()
+      // d
+      case 68:
+        e.preventDefault()
+        return download()
+      // F2
+      case 113:
+        e.preventDefault()
+        return rename()
+      // m
+      case 77:
+        e.preventDefault()
+        return move()
+      // F4
+      case 115:
+        e.preventDefault()
+        return copy()
+      // a
+      case 65:
+        e.preventDefault()
+        return archive()
+      // F8
+      case 119:
+        e.preventDefault()
+        return deleteFile()
+      // Del
+      case 46:
+        e.preventDefault()
+        return deleteFile()
+      // F3
+      case 114:
+        e.preventDefault()
+        return permissions()
+      default:
+        break
     }
   }
 
-  let matchArchive = props.name.match(/.zip|.tgz|.tar.gz|.gzip|.tbz|.tar.bz|.gz|.zip|.tar|.rar/g);
+  let matchArchive = props.name.match(/.zip|.tgz|.tar.gz|.gzip|.tbz|.tar.bz|.gz|.zip|.tar|.rar/g)
 
   return (
     <div className="menu">
@@ -153,30 +190,80 @@ const Menu = (props) => {
       </div>
       <div className="btn-group" role="group" aria-label="First group">
         <input type="file" className="upload" multiple onChange={upload} ref={inputFile} />
-        <button type="button" className="btn btn-light" id="upload" onClick={() => inputFile.current.click()}>{i18n.UPLOAD}</button>
-        <button type="button" className="btn btn-light big" onClick={newFile}>{i18n['NEW FILE']}</button>
-        <button type="button" className="btn btn-light small" onClick={newFile} title={i18n['NEW FILE']}><FontAwesomeIcon icon="file" className="icon file" /></button>
-        <button type="button" className="btn btn-light big" onClick={newDirectory}>{i18n['NEW DIR']}</button>
-        <button type="button" className="btn btn-light small" onClick={newDirectory} title={i18n['NEW DIR']}><FontAwesomeIcon icon="folder" className="icon folder-close" /></button>
-        <button type="button" className="btn btn-light big" onClick={download}>{i18n.DOWNLOAD}</button>
-        <button type="button" className="btn btn-light small" onClick={download} title={i18n.DOWNLOAD}><FontAwesomeIcon icon="download" className="icon download" /></button>
-        <button type="button" className="btn btn-light big" onClick={rename}>{i18n.RENAME}</button>
-        <button type="button" className="btn btn-light small" onClick={rename} title={i18n.RENAME}><FontAwesomeIcon icon="italic" className="icon italic" /></button>
-        <button type="button" className="btn btn-light big" onClick={permissions}>{i18n.RIGHTS}</button>
-        <button type="button" className="btn btn-light small" onClick={permissions} title={i18n.RIGHTS}><FontAwesomeIcon icon="user" className="icon user" /></button>
-        <button type="button" className="btn btn-light big" onClick={copy}>{i18n.COPY}</button>
-        <button type="button" className="btn btn-light small" onClick={copy} title={i18n.COPY}><FontAwesomeIcon icon="copy" className="icon copy" /></button>
-        <button type="button" className="btn btn-light big" onClick={move}>{i18n.MOVE}</button>
-        <button type="button" className="btn btn-light small" onClick={move} title={i18n.MOVE}><FontAwesomeIcon icon="paste" className="icon paste" /></button>
-        {matchArchive ? null : <button type="button" className="btn btn-light big" onClick={archive}>{i18n.ARCHIVE}</button>}
-        {matchArchive ? null : <button type="button" className="btn btn-light small" onClick={archive} title={i18n.ARCHIVE}><FontAwesomeIcon icon="book" className="icon book" /></button>}
-        {matchArchive ? <button type="button" className="btn btn-light big" onClick={extract}>{i18n.EXTRACT}</button> : null}
-        {matchArchive ? <button type="button" className="btn btn-light small" onClick={extract} title={i18n.EXTRACT}><FontAwesomeIcon icon="box-open" className="icon open" /></button> : null}
-        <button type="button" className="btn btn-light big delete" onClick={deleteFile} >{i18n.DELETE}</button>
-        <button type="button" className="btn btn-light small" onClick={deleteFile} title={i18n.DELETE}><FontAwesomeIcon icon="trash" className="icon trash" /></button>
+        <button type="button" className="btn btn-light" id="upload" onClick={() => inputFile.current.click()}>
+          {i18n.UPLOAD}
+        </button>
+        <button type="button" className="btn btn-light big" onClick={newFile}>
+          {i18n['NEW FILE']}
+        </button>
+        <button type="button" className="btn btn-light small" onClick={newFile} title={i18n['NEW FILE']}>
+          <FontAwesomeIcon icon="file" className="icon file" />
+        </button>
+        <button type="button" className="btn btn-light big" onClick={newDirectory}>
+          {i18n['NEW DIR']}
+        </button>
+        <button type="button" className="btn btn-light small" onClick={newDirectory} title={i18n['NEW DIR']}>
+          <FontAwesomeIcon icon="folder" className="icon folder-close" />
+        </button>
+        <button type="button" className="btn btn-light big" onClick={download}>
+          {i18n.DOWNLOAD}
+        </button>
+        <button type="button" className="btn btn-light small" onClick={download} title={i18n.DOWNLOAD}>
+          <FontAwesomeIcon icon="download" className="icon download" />
+        </button>
+        <button type="button" className="btn btn-light big" onClick={rename}>
+          {i18n.RENAME}
+        </button>
+        <button type="button" className="btn btn-light small" onClick={rename} title={i18n.RENAME}>
+          <FontAwesomeIcon icon="italic" className="icon italic" />
+        </button>
+        <button type="button" className="btn btn-light big" onClick={permissions}>
+          {i18n.RIGHTS}
+        </button>
+        <button type="button" className="btn btn-light small" onClick={permissions} title={i18n.RIGHTS}>
+          <FontAwesomeIcon icon="user" className="icon user" />
+        </button>
+        <button type="button" className="btn btn-light big" onClick={copy}>
+          {i18n.COPY}
+        </button>
+        <button type="button" className="btn btn-light small" onClick={copy} title={i18n.COPY}>
+          <FontAwesomeIcon icon="copy" className="icon copy" />
+        </button>
+        <button type="button" className="btn btn-light big" onClick={move}>
+          {i18n.MOVE}
+        </button>
+        <button type="button" className="btn btn-light small" onClick={move} title={i18n.MOVE}>
+          <FontAwesomeIcon icon="paste" className="icon paste" />
+        </button>
+        {matchArchive ? null : (
+          <button type="button" className="btn btn-light big" onClick={archive}>
+            {i18n.ARCHIVE}
+          </button>
+        )}
+        {matchArchive ? null : (
+          <button type="button" className="btn btn-light small" onClick={archive} title={i18n.ARCHIVE}>
+            <FontAwesomeIcon icon="book" className="icon book" />
+          </button>
+        )}
+        {matchArchive ? (
+          <button type="button" className="btn btn-light big" onClick={extract}>
+            {i18n.EXTRACT}
+          </button>
+        ) : null}
+        {matchArchive ? (
+          <button type="button" className="btn btn-light small" onClick={extract} title={i18n.EXTRACT}>
+            <FontAwesomeIcon icon="box-open" className="icon open" />
+          </button>
+        ) : null}
+        <button type="button" className="btn btn-light big delete" onClick={deleteFile}>
+          {i18n.DELETE}
+        </button>
+        <button type="button" className="btn btn-light small" onClick={deleteFile} title={i18n.DELETE}>
+          <FontAwesomeIcon icon="trash" className="icon trash" />
+        </button>
       </div>
     </div>
-  );
+  )
 }
 
-export default Menu;
+export default Menu

+ 23 - 4
src/react/src/components/Modal/AddDirectory.jsx

@@ -1,9 +1,27 @@
-import React from 'react';
+import React, { useState } from 'react';
 import { useSelector } from 'react-redux';
 
 
 const AddDirectory = (props) => {
+  const [value, setValue] = useState(null)
   const { i18n } = useSelector(state => state.session);
+  const [hasError, setHasError] = useState(value !== null && !value.length)
+
+  const onChange = (e) => {
+    setValue(e.target.value)
+  }
+
+  const save = () => {
+    if (!value) {
+      setHasError(true)
+      return;
+    }
+    props.save()
+  }
+
+  const cancel = () => {
+    props.close()
+  }
 
   return (
     <div className="modal-content">
@@ -11,11 +29,12 @@ const AddDirectory = (props) => {
         <h3 className="modal-title directory" >{i18n['Create directory']}</h3>
       </div>
       <div className="modal-body">
-        <input type="text" ref={props.reference} autoFocus></input>
+        <input type="text" onChange={onChange} ref={props.reference}></input>
+        {hasError && <small className='error'>{i18n['Directory name cannot be empty']}</small>}
       </div>
       <div className="modal-footer">
-        <button type="button" className="btn btn-danger mr-auto" onClick={props.close}>{i18n.Cancel}</button>
-        <button type="button" className="btn btn-primary" onClick={props.save}>{i18n.Create}</button>
+        <button type="button" className="btn btn-danger mr-auto" onClick={cancel}>{i18n.Cancel}</button>
+        <button type="button" className="btn btn-primary" onClick={save}>{i18n.Create}</button>
       </div>
     </div>
   );

+ 23 - 4
src/react/src/components/Modal/AddFile.jsx

@@ -1,8 +1,26 @@
-import React from 'react';
+import React, { useState } from 'react';
 import { useSelector } from 'react-redux';
 
 const AddFile = (props) => {
+  const [value, setValue] = useState(null)
   const { i18n } = useSelector(state => state.session);
+  const [hasError, setHasError] = useState(value !== null && !value.length)
+
+  const onChange = (e) => {
+    setValue(e.target.value)
+  }
+
+  const save = () => {
+    if (!value) {
+      setHasError(true)
+      return;
+    }
+    props.save()
+  }
+
+  const cancel = () => {
+    props.close()
+  }
 
   return (
     <div className="modal-content">
@@ -10,11 +28,12 @@ const AddFile = (props) => {
         <h3 className="modal-title" >{i18n['Create file']}</h3>
       </div>
       <div className="modal-body">
-        <input type="text" ref={props.reference}></input>
+        <input type="text" onChange={onChange} ref={props.reference}></input>
+        {hasError && <small className='error'>{i18n['File name cannot be empty']}</small>}
       </div>
       <div className="modal-footer">
-        <button type="button" className="btn btn-danger mr-auto" onClick={props.close}>{i18n.Cancel}</button>
-        <button type="button" className="btn btn-primary" onClick={props.save}>{i18n.Create}</button>
+        <button type="button" className="btn btn-danger mr-auto" onClick={cancel}>{i18n.Cancel}</button>
+        <button type="button" className="btn btn-primary" onClick={save}>{i18n.Create}</button>
       </div>
     </div>
   );

+ 47 - 50
src/react/src/components/Modal/Modal.jsx

@@ -1,4 +1,4 @@
-import React, { Component } from 'react';
+import React, { useEffect } from 'react';
 import AddFile from './AddFile';
 import AddDirectory from './AddDirectory';
 import Rename from './Rename';
@@ -9,86 +9,83 @@ import Move from './Move';
 import Archive from './Archive';
 import Extract from './Extract';
 import Copy from './Copy';
-import './Modal.scss';
 import Replace from './Replace';
+import './Modal.scss';
 
-class Modal extends Component {
-
-  componentDidMount = () => {
-    window.addEventListener("click", this.closeOutside);
-    document.addEventListener("keydown", this.hotkeys);
-  }
+const Modal = (props) => {
+  useEffect(() => {
+    window.addEventListener("click", closeOutside);
+    document.addEventListener("keydown", hotkeys);
 
-  componentWillUnmount = () => {
-    window.removeEventListener("click", this.closeOutside);
-    document.removeEventListener("keydown", this.hotkeys);
-  }
+    return () => {
+      window.removeEventListener("click", closeOutside);
+      document.removeEventListener("keydown", hotkeys);
+    }
+  }, [])
 
-  hotkeys = (e) => {
+  const hotkeys = (e) => {
     if (e.keyCode === 27) {
-      this.closeModal();
+      closeModal();
     } else if (e.keyCode === 13) {
-      this.saveAndClose();
+      saveAndClose();
     }
   }
 
-  saveAndClose = () => {
-    this.props.onClick();
-    this.props.onClose();
+  const saveAndClose = () => {
+    props.onClick();
+    props.onClose();
   }
 
-  changePermissions = (permissions) => {
-    this.props.onChangePermissions(permissions);
+  const changePermissions = (permissions) => {
+    props.onChangePermissions(permissions);
   }
 
-  replace = (file) => {
-    this.props.onClick(file);
-    this.props.onClose();
+  const replace = (file) => {
+    props.onClick(file);
+    props.onClose();
   }
 
-  onChange = (e) => {
-    this.props.onChangeValue(e.target.value);
+  const onChange = (e) => {
+    props.onChangeValue(e.target.value);
   }
 
-  closeModal = () => {
-    this.props.onClose();
+  const closeModal = () => {
+    props.onClose();
   }
 
-  closeOutside = (e) => {
+  const closeOutside = (e) => {
     let modal = document.getElementById("modal");
     if (e.target === modal) {
-      this.props.onClose();
+      props.onClose();
     }
   }
 
-  content = () => {
-    const { type, reference, fName, permissions, items, path, files, notAvailable } = this.props;
+  const content = () => {
+    const { type, reference, fName, permissions, items, path, files, notAvailable } = props;
     switch (type) {
-      case 'Copy': return <Copy close={this.closeModal} save={this.saveAndClose} reference={reference} onChange={this.onChange} name={type} fName={fName} items={items} path={path} />;
-      case 'Move': return <Move close={this.closeModal} save={this.saveAndClose} reference={reference} onChange={this.onChange} name={type} fName={fName} items={items} path={path} />;
-      case 'Permissions': return <Permissions close={this.closeModal} save={this.saveAndClose} changePermissions={this.changePermissions} fName={fName} permissions={permissions} />;
-      case 'Extract': return <Extract close={this.closeModal} save={this.saveAndClose} reference={reference} onChange={this.onChange} name={type} fName={fName} path={path} />;
-      case 'Archive': return <Archive close={this.closeModal} save={this.saveAndClose} reference={reference} onChange={this.onChange} items={items} name={type} fName={fName} path={path} />;
-      case 'Rename': return <Rename close={this.closeModal} save={this.saveAndClose} reference={reference} onChange={this.onChange} name={type} fName={fName} />;
-      case 'Add directory': return <AddDirectory close={this.closeModal} save={this.saveAndClose} reference={reference} />;
-      case 'Delete': return <Delete close={this.closeModal} save={this.saveAndClose} fName={fName} items={items} />;
-      case 'Add file': return <AddFile close={this.closeModal} save={this.saveAndClose} reference={reference} />;
-      case 'Replace': return <Replace close={this.closeModal} replace={(files) => this.replace(files)} files={files} />
-      case 'Nothing selected': return <NothingSelected close={this.closeModal} notAvailable={notAvailable} />;
+      case 'Copy': return <Copy close={closeModal} save={saveAndClose} reference={reference} onChange={onChange} name={type} fName={fName} items={items} path={path} />;
+      case 'Move': return <Move close={closeModal} save={saveAndClose} reference={reference} onChange={onChange} name={type} fName={fName} items={items} path={path} />;
+      case 'Permissions': return <Permissions close={closeModal} save={saveAndClose} changePermissions={changePermissions} fName={fName} permissions={permissions} />;
+      case 'Extract': return <Extract close={closeModal} save={saveAndClose} reference={reference} onChange={onChange} name={type} fName={fName} path={path} />;
+      case 'Archive': return <Archive close={closeModal} save={saveAndClose} reference={reference} onChange={onChange} items={items} name={type} fName={fName} path={path} />;
+      case 'Rename': return <Rename close={closeModal} save={saveAndClose} reference={reference} onChange={onChange} name={type} fName={fName} />;
+      case 'Add directory': return <AddDirectory close={closeModal} save={saveAndClose} reference={reference} />;
+      case 'Delete': return <Delete close={closeModal} save={saveAndClose} fName={fName} items={items} />;
+      case 'Add file': return <AddFile close={closeModal} save={saveAndClose} reference={reference} />;
+      case 'Replace': return <Replace close={closeModal} replace={(files) => replace(files)} files={files} />
+      case 'Nothing selected': return <NothingSelected close={closeModal} notAvailable={notAvailable} />;
       default:
         break;
     }
   }
 
-  render() {
-    return (
-      <div>
-        <div className="modal" id="modal">
-          {this.content()}
-        </div>
+  return (
+    <div>
+      <div className="modal" id="modal">
+        {content()}
       </div>
-    );
-  }
+    </div>
+  );
 }
 
 export default Modal;

+ 24 - 9
src/react/src/components/Modal/Modal.scss

@@ -12,7 +12,7 @@
   height: 100%;
   overflow: auto;
   background-color: $black;
-  background-color: rgba(0,0,0,0.4);
+  background-color: rgba(0, 0, 0, 0.4);
 
   .modal-content {
     box-shadow: 0 2px 11px 0 rgba(0, 0, 0, 0.5);
@@ -25,10 +25,23 @@
     margin-top: 100px;
 
     .modal-body {
+      display: flex;
+      flex-direction: column;
+      justify-content: center;
+      align-items: center;
       overflow-wrap: anywhere;
       font-size: 17px;
       margin-bottom: 40px;
 
+      small {
+        margin-top: 5px;
+        font-size: 13px;
+        
+        &.error {
+          color: red;
+        }
+      }
+
       input {
         background: #333;
         border: 1px solid #111;
@@ -44,6 +57,7 @@
       word-break: break-word;
 
       h3 {
+        font-size: 20px;
         color: $secondaryLight;
       }
 
@@ -66,7 +80,7 @@
         border-color: none;
         background: none;
         text-transform: uppercase;
-        font-size: 12px;
+        font-size: 11px;
 
         &:focus {
           outline: none;
@@ -98,7 +112,7 @@
           border-color: $primaryLight;
           color: $hoverButtonText;
         }
-        
+
         &:hover {
           background: $primaryActive;
           border-color: $primaryActive;
@@ -106,7 +120,7 @@
         }
       }
     }
-    
+
     .header .quot {
       color: $secondaryActive;
     }
@@ -144,17 +158,18 @@
       right: 15px;
     }
   }
-  
+
   .permissions {
     height: auto;
     width: 30%;
     margin-top: 0;
-    
-    .error, &:focus {
+
+    .error,
+    &:focus {
       border: 1px solid red;
     }
 
-    input[type="text"] {
+    input[type='text'] {
       size: 40px;
       width: 60px;
       margin: auto auto 10px auto;
@@ -189,4 +204,4 @@
       }
     }
   }
-}
+}

+ 1 - 1
src/react/src/components/Preview/Preview.jsx

@@ -21,7 +21,7 @@ const Preview = (props) => {
 
   const hotkeys = e => {
     if (e.keyCode === 121) {
-      props.onClose();
+      onClose();
     }
   }
 

+ 12 - 2
src/react/src/components/Server/Server.scss

@@ -14,6 +14,16 @@
   }
 }
 
+.servers-wrapper .l-col {
+  display: flex;
+  align-items: center;
+  height: 55px;
+
+  .checkbox {
+    margin: 0px;
+  }
+}
+
 .servers-wrapper .r-col {
   padding-left: 2rem;
 
@@ -28,7 +38,7 @@
     transform: translateX(-10px);
   }
 
-  .stats div>span {
+  .stats div > span {
     width: 100%;
   }
-}
+}

+ 11 - 0
src/react/src/containers/App/App.scss

@@ -222,6 +222,17 @@ button {
       }
     }
   }
+
+  .content .servers-list .servers-wrapper .l-col {
+    display: flex;
+    align-items: center;
+    height: 55px;
+    margin-top: .75rem;
+
+    .checkbox {
+      margin: 0px;
+    }
+  }
 }
 
 @media (max-width: 900px) {

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
web/static/css/main.55ab5a88.chunk.css


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
web/static/css/main.55ab5a88.chunk.css.map


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
web/static/css/main.9f0c683e.chunk.css


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
web/static/css/main.9f0c683e.chunk.css.map


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
web/static/index.html


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 1
web/static/js/2.5dc90ea3.chunk.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
web/static/js/2.5dc90ea3.chunk.js.map


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 1 - 0
web/static/js/2.7cb4195c.chunk.js


+ 0 - 0
web/static/js/2.5dc90ea3.chunk.js.LICENSE.txt → web/static/js/2.7cb4195c.chunk.js.LICENSE.txt


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
web/static/js/2.7cb4195c.chunk.js.map


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
web/static/js/main.57f35a42.chunk.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
web/static/js/main.57f35a42.chunk.js.map


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
web/static/js/main.a9be926e.chunk.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
web/static/js/main.a9be926e.chunk.js.map


Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно