diff --git a/lib/files.js b/lib/files.js index 093b9b5..160ae22 100644 --- a/lib/files.js +++ b/lib/files.js @@ -128,17 +128,17 @@ class Files describe() { // Set up - const result = {} + const result = {} result.count = this.count - result.items = {} + result.items = [] // Each file for(const _filePath in this.items) { const file = this.items[_filePath] - result.items[_filePath] = file.describe() + result.items.push(file.describe()) } return result diff --git a/site/src/components/application/script.js b/site/src/components/application/script.js index dafe547..a0389d0 100644 --- a/site/src/components/application/script.js +++ b/site/src/components/application/script.js @@ -1,5 +1,6 @@ import Connexion from '@/components/connexion' import ProjectsHub from '@/components/projects-hub' +import Project from '@/components/project' export default { @@ -8,7 +9,8 @@ export default components: { Connexion, - ProjectsHub + ProjectsHub, + Project }, data() @@ -27,7 +29,7 @@ export default project() { - return this.$store.state.currentProject + return this.$store.state.project } }, diff --git a/site/src/components/application/template.html b/site/src/components/application/template.html index 085c035..0a05315 100644 --- a/site/src/components/application/template.html +++ b/site/src/components/application/template.html @@ -2,7 +2,5 @@ {{ counter }} -
- {{ project.name }} -
+ \ No newline at end of file diff --git a/site/src/components/connexion/script.js b/site/src/components/connexion/script.js index 689f432..d1b05ac 100644 --- a/site/src/components/connexion/script.js +++ b/site/src/components/connexion/script.js @@ -4,19 +4,71 @@ export default { name: 'connexion', + computed: + { + project() + { + return this.$store.state.project + } + }, + + watch: + { + project: 'onProjectChange' + }, + created() { - this.url = `${process.env.NODE_ENV === 'production' ? '' : 'http://localhost:1571'}/projects` - this.socket = socketIoClient(this.url) + const projectsUrl = `${process.env.NODE_ENV === 'production' ? '' : 'http://localhost:1571'}/projects` + this.projectsSocket = socketIoClient(projectsUrl) - this.socket.on('connect', () => + this.projectsSocket.on('connect', () => { - console.log('connected') + // console.log('projects connected') }) - this.socket.on('update_projects', (data) => + this.projectsSocket.on('update_projects', (data) => { + // console.log('update_projects', data) this.$store.commit('updateProjects', data.all) }) + }, + + methods: + { + onProjectChange(value) + { + const projectURL = `${process.env.NODE_ENV === 'production' ? '' : 'http://localhost:1571'}/project/${value.slug}` + + this.projectSocket = socketIoClient(projectURL) + + this.projectSocket.on('connect', () => + { + // console.log('project connected') + }) + + this.projectSocket.on('update_project', (data) => + { + // console.log('update_project', data) + this.$store.commit('updateFiles', data.files.items) + }) + + this.projectSocket.on('create_file', (data) => + { + // console.log('create_file', data) + this.$store.commit('createFile', data) + }) + + this.projectSocket.on('delete_file', (data) => + { + // console.log('delete_file', data) + this.$store.commit('deleteFile', data) + }) + + this.projectSocket.on('createVersion', (data) => + { + // console.log('createVersion', data) + }) + } } } \ No newline at end of file diff --git a/site/src/components/files-tree-file/index.vue b/site/src/components/files-tree-file/index.vue new file mode 100644 index 0000000..a6fe49a --- /dev/null +++ b/site/src/components/files-tree-file/index.vue @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/site/src/components/files-tree-file/script.js b/site/src/components/files-tree-file/script.js new file mode 100644 index 0000000..eeadae4 --- /dev/null +++ b/site/src/components/files-tree-file/script.js @@ -0,0 +1,18 @@ +export default +{ + name: 'files-tree-file', + + props: + { + depth: { type: Number, default: 0 }, + content: { type: Object } + }, + + created() + { + }, + + methods: + { + } +} \ No newline at end of file diff --git a/site/src/components/files-tree-file/style.styl b/site/src/components/files-tree-file/style.styl new file mode 100644 index 0000000..e69de29 diff --git a/site/src/components/files-tree-file/template.html b/site/src/components/files-tree-file/template.html new file mode 100644 index 0000000..bc4d6e3 --- /dev/null +++ b/site/src/components/files-tree-file/template.html @@ -0,0 +1,5 @@ +
+
+ {{ content.name }} +
+
\ No newline at end of file diff --git a/site/src/components/files-tree-folder/index.vue b/site/src/components/files-tree-folder/index.vue new file mode 100644 index 0000000..a6fe49a --- /dev/null +++ b/site/src/components/files-tree-folder/index.vue @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/site/src/components/files-tree-folder/script.js b/site/src/components/files-tree-folder/script.js new file mode 100644 index 0000000..2ec245f --- /dev/null +++ b/site/src/components/files-tree-folder/script.js @@ -0,0 +1,24 @@ +import FilesTreeFile from '@/components/files-tree-file' +export default +{ + name: 'files-tree-folder', + + components: + { + FilesTreeFile + }, + + props: + { + depth: { type: Number, default: 0 }, + content: { type: Object } + }, + + created() + { + }, + + methods: + { + } +} \ No newline at end of file diff --git a/site/src/components/files-tree-folder/style.styl b/site/src/components/files-tree-folder/style.styl new file mode 100644 index 0000000..e69de29 diff --git a/site/src/components/files-tree-folder/template.html b/site/src/components/files-tree-folder/template.html new file mode 100644 index 0000000..bd36e35 --- /dev/null +++ b/site/src/components/files-tree-folder/template.html @@ -0,0 +1,7 @@ +
+
+ {{ content.name }} +
+ + +
\ No newline at end of file diff --git a/site/src/components/files-tree/index.vue b/site/src/components/files-tree/index.vue new file mode 100644 index 0000000..a6fe49a --- /dev/null +++ b/site/src/components/files-tree/index.vue @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/site/src/components/files-tree/script.js b/site/src/components/files-tree/script.js new file mode 100644 index 0000000..d50cfde --- /dev/null +++ b/site/src/components/files-tree/script.js @@ -0,0 +1,60 @@ +import FilesTreeFile from '@/components/files-tree-file' +import FilesTreeFolder from '@/components/files-tree-folder' +import FileTree from '@/utils/FileTree' + +// const fileTree = new FileTree({ autoWash: true }) +// fileTree.addFolder('./toto', { hey: 'hoy' }) +// fileTree.addFolder('./toto/uhuh', { hey: 'a' }) +// fileTree.addFolder('./lorem') +// fileTree.addFile('./test-1.txt') +// fileTree.addFile('./test-2.txt') + +// window.setInterval(() => +// { +// fileTree.addFile('./toto/uhuh/' + new Date()) +// fileTree.removeFolder('./lorem') +// fileTree.removeFile('./test-1.txt') +// }, 1000) + +export default +{ + name: 'files-tree', + + components: + { + FilesTreeFile, + FilesTreeFolder + }, + + data() + { + return {} + }, + + computed: + { + files() + { + return this.$store.state.files + } + }, + + watch: + { + files: 'onFilesChange' + }, + + created() + { + }, + + methods: + { + onFilesChange(newValue, oldValue) + { + console.log('onFilesChange') + console.log(newValue) + console.log(oldValue) + } + } +} \ No newline at end of file diff --git a/site/src/components/files-tree/style.styl b/site/src/components/files-tree/style.styl new file mode 100644 index 0000000..e69de29 diff --git a/site/src/components/files-tree/template.html b/site/src/components/files-tree/template.html new file mode 100644 index 0000000..c2aaad2 --- /dev/null +++ b/site/src/components/files-tree/template.html @@ -0,0 +1,3 @@ +
+ +
\ No newline at end of file diff --git a/site/src/components/project/index.vue b/site/src/components/project/index.vue new file mode 100644 index 0000000..a6fe49a --- /dev/null +++ b/site/src/components/project/index.vue @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/site/src/components/project/script.js b/site/src/components/project/script.js new file mode 100644 index 0000000..a71250d --- /dev/null +++ b/site/src/components/project/script.js @@ -0,0 +1,27 @@ +import FilesTree from '@/components/files-tree' + +export default +{ + name: 'project', + + components: + { + FilesTree + }, + + computed: + { + project() + { + return this.$store.state.project + } + }, + + created() + { + }, + + methods: + { + } +} \ No newline at end of file diff --git a/site/src/components/project/style.styl b/site/src/components/project/style.styl new file mode 100644 index 0000000..e69de29 diff --git a/site/src/components/project/template.html b/site/src/components/project/template.html new file mode 100644 index 0000000..56262ca --- /dev/null +++ b/site/src/components/project/template.html @@ -0,0 +1,4 @@ +
+ {{ project.name }} + +
\ No newline at end of file diff --git a/site/src/components/projects-hub/script.js b/site/src/components/projects-hub/script.js index 06fc99d..d00e5c0 100644 --- a/site/src/components/projects-hub/script.js +++ b/site/src/components/projects-hub/script.js @@ -19,7 +19,7 @@ export default { onProjectClick(projectSlug) { - this.$store.commit('setCurrentProject', projectSlug) + this.$store.commit('setProject', projectSlug) } } } \ No newline at end of file diff --git a/site/src/store.js b/site/src/store.js index 471b758..6f44b51 100644 --- a/site/src/store.js +++ b/site/src/store.js @@ -1,27 +1,34 @@ import Vue from 'vue' import Vuex from 'vuex' +import FileTree from '@/utils/FileTree' + Vue.use(Vuex) export default new Vuex.Store({ state: { projects: {}, - currentProject: null + project: null, + files: new FileTree({ autoWash: true }), + file: null }, mutations: { + /** + * Projects + */ updateProjects(state, data) { state.projects = data // Has a current project - if(state.currentProject) + if(state.project) { // Current project has been removed if(typeof state.projects[data] === 'undefined') { - state.currentProject = null + state.project = null } } @@ -33,7 +40,7 @@ export default new Vuex.Store({ // If only one project, set has current if(projectKeys.length === 1) { - state.currentProject = state.projects[projectKeys[0]] + state.project = state.projects[projectKeys[0]] } } }, @@ -43,13 +50,36 @@ export default new Vuex.Store({ state.projects.push(data) }, - setCurrentProject(state, data) + setProject(state, data) { // Test of project exist if(typeof state.projects[data] !== 'undefined') { - state.currentProject = state.projects[data] + state.project = state.projects[data] } + }, + + /** + * Files + */ + updateFiles(state, data) + { + state.files = new FileTree({ autoWash: true }) + + for(const file of data) + { + state.files.addFile(file.path.full, file) + } + }, + + createFile(state, data) + { + state.files.addFile(data.path.full, data) + }, + + deleteFile(state, data) + { + state.files.removeFile(data.path.full) } } }) \ No newline at end of file diff --git a/site/src/utils/FileTree.js b/site/src/utils/FileTree.js new file mode 100644 index 0000000..07bd2bf --- /dev/null +++ b/site/src/utils/FileTree.js @@ -0,0 +1,688 @@ +class FileTree +{ + /** + * Create tree and add ./ folder + * @param {Object} _options + * @param {Boolean} _options.auto_wash - Should clean empty folder on removes + */ + constructor(_options = {}) + { + // Options + this.autoWash = typeof _options.autoWash === 'undefined' ? false : _options.autoWash + + // Set up + this.folders = [] + this.addFolder('.', {}) + } + + /** + * Clean path + * @param {String} _path - Path to clean + * @return {String} Cleaned path + */ + cleanPath(_path = '') + { + // Errors + if(typeof _path !== 'string') + { + console.warn('cleanPath: _path should be a string') + return false + } + + let path = _path + + // Trim + path = path.trim() + + // Repeating `/` + path = path.replace(/\/+/g, '/') + + // Ending `/` + path = path.replace(/\/$/, '') + + console.log(path) + + // Starting `.` + path = path.replace(/^\./, '') + + // Starting `/` + path = path.replace(/^\//, '') + + path = './' + path + + return path + } + + /** + * Add a folder + * Can create nested folder in one path + * @param {String} _path - Path to the folder (start with `./`) + * @param {Object} _data - Properties to add to the folder + * @returns {Object} Created folder + */ + addFolder(_path = '', _data = {}) + { + // Errors + if(typeof _path !== 'string') + { + console.warn('addFolder: _path should be a string') + return false + } + if(typeof _data !== 'object') + { + console.warn('addFolder: _data should be an object') + return false + } + + // Set up + const path = this.cleanPath(_path) + const pathParts = path.split('/') + + let folders = this.folders + let folder = null + + // Each path part + for(const _part of pathParts) + { + const index = folders.findIndex((folder) => _part === folder.name) + + // Already exist + if(index !== -1) + { + folder = folders[index] + folders = folder.folders + } + + // Folder doesn't exist + else + { + // Create folder + folder = { + folders: [], + files: [], + name: _part, + data: _data + } + + // Save + folders.push(folder) + folders = folder.folders + } + } + + // Return + return folder + } + + /** + * Add a file + * Will create folders if needed + * @param {String} _path - Path to the folder (start with `./`) + * @param {Object} _data - Properties to add to the file + * @returns {Object} Created file + */ + addFile(_path = '', _data = {}) + { + // Errors + if(typeof _path !== 'string') + { + console.warn('addFile: _path should be a string') + return false + } + if(typeof _data !== 'object') + { + console.warn('addFile: _data should be an object') + return false + } + + // Set up + const path = this.cleanPath(_path) + const pathParts = path.split('/') + const filePart = pathParts.pop() + + // Create folder + const folder = this.addFolder(pathParts.join('/')) + + // Create file + const file = { + name: filePart, + data: _data + } + + // Save + folder.files.push(file) + + return file + } + + /** + * Remove folder + * Will delete contained folders and contained files + * @param {String} _path - Folder to delete (start with `./`) + * @returns {Boolean} Folder deleted or not + */ + removeFolder(_path = '') + { + // Recursive emptying + const emptyFolder = function(folder) + { + // Delete folders + for(const _folderKey in folder.folders) + { + const _folder = folder.folders[_folderKey] + + emptyFolder(_folder) + + folder.folders.splice(_folderKey, 1) + + // Callback + if(typeof _folder.data.onRemove === 'function') + { + _folder.data.onRemove.apply(this, [_folder]) + } + } + + // Delete files + for(const _fileKey in folder.files) + { + const _file = folder.files[_fileKey] + + folder.files.splice(_fileKey, 1) + + // Callback + if(typeof _file.data.onRemove === 'function') + { + _file.data.onRemove.apply(this, [_file]) + } + } + } + + // Errors + if(typeof _path !== 'string') + { + console.warn('removeFolder: _path should be a string') + return false + } + + // Set up + const path = this.cleanPath(_path) + const pathParts = path.split('/') + const folderPart = pathParts.pop() + + let folders = this.folders + let folder = null + + // Each path part + for(const _part of pathParts) + { + const index = folders.findIndex((folder) => _part === folder.name) + + // Found + if(index !== -1) + { + folder = folders[index] + folders = folder.folders + } + + // Not found + else + { + folder = null + folders = null + break + } + } + + // Found + const index = folders.findIndex((folder) => folderPart === folder.name) + if(folders && index !== -1) + { + const folder = folders[index] + + // Delete + emptyFolder(folder) + folders.splice(index, 1) + + // Callback + if(typeof folder.data.onRemove === 'function') + { + folder.data.onRemove.apply(this, [folder]) + } + + // Auto wash + if(this.autoWash) + { + this.removeEmptyFolders() + } + + return true + } + + return false + } + + /** + * Remove file + * Will delete contained folders and contained files + * @param {String} _path - File to delete (start with `./`) + * @returns {Boolean} File deleted or not + */ + removeFile(_path = '') + { + // Errors + if(typeof _path !== 'string') + { + console.warn('removeFile: _path should be a string') + return false + } + + // Set up + const path = this.cleanPath(_path) + const pathParts = path.split('/') + const filePart = pathParts.pop() + + let folders = this.folders + let folder = null + + // Each path part + for(const _part of pathParts) + { + const index = folders.findIndex((folder) => _part === folder.name) + + // Found + if(index !== -1) + { + folder = folders[index] + folders = folder.folders + } + + // Not found + else + { + folder = null + folders = null + break + } + } + + // Folder found + if(folders && folder) + { + const index = folder.files.findIndex((file) => filePart === file.name) + + // File found + if(index !== -1) + { + const file = folder.files[index] + + // Delete + folder.files.splice(index, 1) + + // Auto wash + if(this.autoWash) + { + this.removeEmptyFolders() + } + + // Callback + if(typeof file.data.onRemove === 'function') + { + file.data.onRemove.apply(this, [file]) + } + + return true + } + } + + return false + } + + /** + * Get file + * @param {String} _path - Path to file + * @returns {Object} File + */ + getFile(_path = '') + { + // Errors + if(typeof _path !== 'string') + { + console.warn('getFile: _path should be a string') + return false + } + + // Set up + const path = this.cleanPath(_path) + const pathParts = path.split('/') + const filePart = pathParts.pop() + + let folders = this.folders + let folder = null + + // Each path part + for(const _part of pathParts) + { + const index = folders.findIndex((folder) => _part === folder.name) + + // Found + if(index !== -1) + { + folder = folders[index] + folders = folder.folders + } + + // Not found + else + { + folder = null + folders = null + break + } + } + + // Folder found + if(folders && folder) + { + const index = folder.files.findIndex((file) => filePart === file.name) + + if(index !== -1) + { + const file = folder.files[index] + return file + } + } + + return false + } + + /** + * Get folder + * @param {String} _path - Path to folder + * @returns {Object} Folder + */ + getFolder(_path = '') + { + // Errors + if(typeof _path !== 'string') + { + console.warn('getFolder: _path should be a string') + return false + } + + // Set up + const path = this.cleanPath(_path) + const pathParts = path.split('/') + + let folders = this.folders + let folder = null + + // Each path part + for(const _part of pathParts) + { + const index = folders.findIndex((folder) => _part === folder.name) + + // Found + if(index !== -1) + { + folder = folders[index] + folders = folder.folders + } + + // Folder doesn't exist + else + { + return false + } + } + + // Return + return folder + } + + /** + * Browse every folders and remove empty ones + * @return {Number} Number of removed folders + */ + removeEmptyFolders() + { + // Set up + let removedCount = 0 + + // Recursive remove + const canRemoveFolder = function(folder) + { + // Each folder inside current folder + for(const _folderKey in folder.folders) + { + // Try to remove folder + const _folder = folder.folders[_folderKey] + const canRemove = canRemoveFolder(_folder) + + // Remove folder + if(canRemove) + { + removedCount++ + + // Delete + folder.folders.splice(_folderKey, 1) + + // Callback + if(typeof _folder.data.onRemove === 'function') + { + _folder.data.onRemove.apply(this, [_folder]) + } + } + } + + // Can be removed + const folderKeys = Object.keys(folder.folders) + const filesKeys = Object.keys(folder.files) + + if(folderKeys.length === 0 && filesKeys.length === 0) + { + return true + } + else + { + return false + } + } + + // Try from ./ + canRemoveFolder(this.folders[0]) + + return removedCount + } + + /** + * Describe the tree in ASCII (├ ─ │ └) + * @param {Boolean} _log - Directly log to console + * @param {Boolean} _colored - Colored tree (only work well in Chrome) + * @return {String} Tree + */ + describe(_log = false, _colored = false) + { + // Set up + const depth = 0 + const styles = [] + + let stringTree = '' + + const addToString = function(value = '', type = null) + { + if(_colored) + { + stringTree += '%c' + + switch(type) + { + case 'structure': + styles.push('color:#999;') + break + + case 'folder': + styles.push('color:#999;') + break + case 'file': + styles.push('color:#333;font-weight:bold;') + break + + default: + styles.push('') + break + } + } + + stringTree += value + } + + // Recursive describe + const describeFolder = function(folder, depth, last = []) + { + // Each folders + for(let i = 0; i < folder.folders.length; i++) + { + // Set up + const _folder = folder.folders[i] + + // Add to tree string + addToString('\n') + + for(let j = 0; j < depth; j++) + { + if(j === depth - 1) + { + if(i === folder.folders.length - 1 && folder.files.length === 0) + { + addToString(' └', 'structure') + } + else + { + addToString(' ├', 'structure') + } + } + else + { + if(last[j]) + { + addToString(' ', 'structure') + } + else + { + addToString(' │', 'structure') + } + } + } + + addToString('─', 'structure') + addToString(_folder.name, 'folder') + addToString('/', 'structure') + + // Last + last.push(i === folder.folders.length - 1 && folder.files.length === 0) + + // Continue + describeFolder(_folder, depth + 1, last) + } + + // Each files + for(let i = 0; i < folder.files.length; i++) + { + // Set up + const _file = folder.files[i] + + // Add to tree string + stringTree += '\n' + + for(let j = 0; j < depth; j++) + { + if(j === depth - 1) + { + if(i === folder.files.length - 1) + { + addToString(' └', 'structure') + } + else + { + addToString(' ├', 'structure') + } + } + else + { + if(last[j]) + { + addToString(' ', 'structure') + } + else + { + addToString(' │', 'structure') + } + } + } + + addToString('─', 'structure') + addToString(_file.name, 'file') + } + } + + // Describe from ./ + addToString('.', 'folder') + addToString('/', 'structure') + describeFolder(this.folders[0], depth + 1) + + // Log + if(_log) + { + console.log(...[stringTree].concat(styles)) + } + + return stringTree + } +} + + +// /** +// * Tests +// */ +// const filesTree = new FileTree() + +// filesTree.addFolder('./hey/hoy', { active: false, notifs: 0 }) +// filesTree.addFolder('./hey/hoy/toto', { active: false, notifs: 0 }) +// filesTree.addFolder('./hey/hoy/tata', { active: false, notifs: 0 }) + +// filesTree.addFile('./test-1.txt', { active: false, notifs: 0 }) +// filesTree.addFile('./hey/hoy/test-2.txt', { active: false, notifs: 0 }) +// filesTree.addFile('./hey/hoy/test-3.txt', { active: false, notifs: 0 }) +// filesTree.addFile('./hey/hoy/tata/test-4.txt', { active: false, notifs: 0 }) +// filesTree.addFile('./hey/hoy/toto/test-5.txt', { active: false, notifs: 0 }) + +// filesTree.addFile('./hey/hoy/toto/test-6.txt', { +// active: false, +// notifs: 0, +// onRemove: (file) => +// { +// console.log('removed file :', file) +// } +// }) +// filesTree.removeFile('./hey/hoy/toto/test-6.txt') + +// filesTree.addFolder('./pwet', { +// onRemove: (folder) => +// { +// console.log('removed folder :', folder) +// } +// }) +// filesTree.addFolder('./pwet/uh', { +// onRemove: (folder) => +// { +// console.log('removed folder :', folder) +// } +// }) +// filesTree.removeFolder('./pwet') + +// filesTree.removeFile('./hey/hoy/test-2.txt') +// filesTree.removeFolder('./hey') + +// filesTree.describe(true, true) + +export default FileTree \ No newline at end of file