Browse Source

feat: lists UX + assets create folder UI (wip)

Nick 6 years ago
parent
commit
7b08c8bb31

+ 26 - 0
client/components/editor/editor-markdown.vue

@@ -396,6 +396,13 @@ export default {
       }
       return lvl
     },
+    /**
+     * Insert content at cursor
+     */
+    insertAtCursor({ content }) {
+      const cursor = this.cm.doc.getCursor('head')
+      this.cm.doc.replaceRange(content, cursor)
+    },
     /**
      * Insert content after current line
      */
@@ -457,6 +464,25 @@ export default {
     toggleFullscreen () {
       this.cm.setOption('fullScreen', true)
     }
+  },
+  mounted() {
+    this.$root.$on('editorInsert', opts => {
+      switch (opts.kind) {
+        case 'IMAGE':
+          this.insertAtCursor({
+            content: `![${opts.text}](${opts.path})`
+          })
+          break
+        case 'BINARY':
+          this.insertAtCursor({
+            content: `[${opts.text}](${opts.path})`
+          })
+          break
+      }
+    })
+  },
+  beforeDestroy() {
+    this.$root.$off('editorInsert')
   }
 }
 </script>

+ 6 - 1
client/components/editor/editor-modal-blocks.vue

@@ -2,7 +2,7 @@
   v-card.editor-modal-blocks.animated.fadeInLeft(flat, tile)
     v-container.pa-3(grid-list-lg, fluid)
       v-layout(row, wrap)
-        v-flex(xs3)
+        v-flex(xs12, lg4, xl3)
           v-card.radius-7(light)
             v-card-text
               .d-flex
@@ -82,5 +82,10 @@ export default {
     width: calc(100vw - 64px - 17px);
     height: calc(100vh - 112px - 24px);
     background-color: rgba(darken(mc('grey', '900'), 3%), .9) !important;
+
+    @include until($tablet) {
+      left: 40px;
+      width: calc(100vw - 40px);
+    }
 }
 </style>

+ 64 - 19
client/components/editor/editor-modal-media.vue

@@ -11,9 +11,30 @@
                 v-btn.ml-3.my-0.radius-7(outline, large, color='teal', disabled, :icon='$vuetify.breakpoint.xsOnly')
                   v-icon(:left='$vuetify.breakpoint.mdAndUp') keyboard_arrow_up
                   span.hidden-sm-and-down Parent Folder
-                v-btn.my-0.mr-0.radius-7(outline, large, color='teal', :icon='$vuetify.breakpoint.xsOnly')
-                  v-icon(:left='$vuetify.breakpoint.mdAndUp') add
-                  span.hidden-sm-and-down New Folder
+                v-dialog(v-model='newFolderDialog', max-width='550')
+                  v-btn.my-0.mr-0.radius-7(outline, large, color='teal', :icon='$vuetify.breakpoint.xsOnly', slot='activator')
+                    v-icon(:left='$vuetify.breakpoint.mdAndUp') add
+                    span.hidden-sm-and-down New Folder
+                  v-card.wiki-form
+                    .dialog-header.is-short New Folder
+                    v-card-text
+                      v-text-field.md2(
+                        outline
+                        background-color='grey lighten-3'
+                        prepend-icon='folder'
+                        v-model='newFolderName'
+                        label='Folder Name'
+                        counter='255'
+                        @keyup.enter='createFolder'
+                        @keyup.esc='newFolderDialog = false'
+                        ref='folderNameIpt'
+                        hint='Lowercase. No spaces allowed.'
+                        persistent-hint
+                        )
+                    v-card-chin
+                      v-spacer
+                      v-btn(flat, @click='newFolderDialog = false') Cancel
+                      v-btn(color='primary', @click='createFolder', :disabled='!isFolderNameValid') Create
               v-data-table(
                 :items='assets'
                 :headers='headers'
@@ -26,40 +47,40 @@
                 template(slot='items', slot-scope='props')
                   tr.is-clickable(
                     @click.left='currentFileId = props.item.id'
-                    @click.right=''
+                    @click.right.prevent=''
                     :class='currentFileId === props.item.id ? `teal lighten-5` : ``'
                     )
                     td.text-xs-right(v-if='$vuetify.breakpoint.smAndUp') {{ props.item.id }}
                     td
                       .body-2(:class='currentFileId === props.item.id ? `teal--text` : ``') {{ props.item.filename }}
-                      .caption {{ props.item.description }}
+                      .caption.grey--text {{ props.item.description }}
                     td.text-xs-center(v-if='$vuetify.breakpoint.lgAndUp')
-                      v-chip(small, :color='$vuetify.dark ? `grey darken-4` : `grey lighten-4`')
+                      v-chip.ma-0(small, :color='$vuetify.dark ? `grey darken-4` : `grey lighten-4`')
                         .caption {{props.item.ext.toUpperCase().substring(1)}}
                     td(v-if='$vuetify.breakpoint.mdAndUp') {{ props.item.fileSize | prettyBytes }}
-                    td(v-if='$vuetify.breakpoint.mdAndUp') {{ props.item.updatedAt | moment('from') }}
+                    td(v-if='$vuetify.breakpoint.mdAndUp') {{ props.item.createdAt | moment('from') }}
                     td(v-if='$vuetify.breakpoint.smAndUp')
                       v-menu(offset-x)
-                        v-btn(icon, slot='activator')
+                        v-btn.ma-0(icon, slot='activator')
                           v-icon(color='grey darken-2') more_horiz
                         v-list.py-0
-                          v-list-tile
+                          v-list-tile(@click='')
                             v-list-tile-avatar
                               v-icon(color='teal') short_text
                             v-list-tile-content Properties
                           v-divider
                           template(v-if='props.item.kind === `IMAGE`')
-                            v-list-tile
+                            v-list-tile(@click='')
                               v-list-tile-avatar
                                 v-icon(color='indigo') crop_rotate
                               v-list-tile-content Edit
                             v-divider
-                          v-list-tile
+                          v-list-tile(@click='')
                             v-list-tile-avatar
                               v-icon(color='blue') keyboard
                             v-list-tile-content Rename / Move
                           v-divider
-                          v-list-tile
+                          v-list-tile(@click='')
                             v-list-tile-avatar
                               v-icon(color='red') delete
                             v-list-tile-content Delete
@@ -147,6 +168,8 @@ import 'filepond/dist/filepond.min.css'
 import listAssetQuery from 'gql/editor/editor-media-query-list.gql'
 
 const FilePond = vueFilePond()
+const localeSegmentRegex = /^[A-Z]{2}(-[A-Z]{2})?$/i
+const disallowedFolderChars = /[A-Z()=.!@#$%?&*+`~<>,;:\\/[\]¬{| ]/
 
 export default {
   components: {
@@ -172,7 +195,9 @@ export default {
       ],
       imageAlignment: '',
       currentFileId: null,
-      loading: false
+      loading: false,
+      newFolderDialog: false,
+      newFolderName: ''
     }
   },
   computed: {
@@ -191,12 +216,24 @@ export default {
     headers() {
       return _.compact([
         this.$vuetify.breakpoint.smAndUp && { text: 'ID', value: 'id', width: 50, align: 'right' },
-        { text: 'Title', value: 'title' },
-        this.$vuetify.breakpoint.lgAndUp && { text: 'Type', value: 'path', width: 50 },
-        this.$vuetify.breakpoint.mdAndUp && { text: 'File Size', value: 'createdAt', width: 150 },
-        this.$vuetify.breakpoint.mdAndUp && { text: 'Last Updated', value: 'updatedAt', width: 150 },
-        this.$vuetify.breakpoint.smAndUp && { text: '', value: '', width: 50, sortable: false }
+        { text: 'Filename', value: 'filename' },
+        this.$vuetify.breakpoint.lgAndUp && { text: 'Type', value: 'ext', width: 50 },
+        this.$vuetify.breakpoint.mdAndUp && { text: 'File Size', value: 'fileSize', width: 110 },
+        this.$vuetify.breakpoint.mdAndUp && { text: 'Added', value: 'createdAt', width: 150 },
+        this.$vuetify.breakpoint.smAndUp && { text: 'Actions', value: '', width: 40, sortable: false, align:'right' }
       ])
+    },
+    isFolderNameValid() {
+      return this.newFolderName.length > 1 && !localeSegmentRegex.test(this.newFolderName) && !disallowedFolderChars.test(this.newFolderName)
+    }
+  },
+  watch: {
+    newFolderDialog(newValue, oldValue) {
+      if (newValue) {
+        this.$nextTick(() => {
+          this.$refs.folderNameIpt.focus()
+        })
+      }
     }
   },
   filters: {
@@ -227,8 +264,13 @@ export default {
   },
   methods: {
     insert () {
+      const asset = _.find(this.assets, ['id', this.currentFileId])
+      this.$root.$emit('editorInsert', {
+        kind: asset.kind,
+        path: `/${asset.filename}`,
+        text: asset.filename
+      })
       this.activeModal = ''
-
     },
     browse () {
       this.$refs.pond.browse()
@@ -262,6 +304,9 @@ export default {
       }, 5000)
 
       await this.$apollo.queries.assets.refetch()
+    },
+    async createFolder() {
+
     }
   },
   apollo: {

+ 6 - 0
client/scss/components/v-dialog.scss

@@ -26,6 +26,12 @@
     background-image: radial-gradient(ellipse at top, mc('grey', '800'), mc('grey', '900')),
                       radial-gradient(ellipse at bottom, mc('grey', '800'), mc('grey', '900'));
   }
+
+  &.is-teal {
+    background-color: mc('teal', '700');
+    background-image: radial-gradient(ellipse at top, mc('teal', '500'), mc('teal', '700')),
+                      radial-gradient(ellipse at bottom, mc('teal', '800'), mc('teal', '700'));
+  }
 }
 
 .v-dialog--fullscreen {

+ 91 - 0
client/themes/default/scss/app.scss

@@ -235,6 +235,92 @@
     li + li {
       margin-top: .5rem;
     }
+
+    &.links-list {
+      li {
+        background-color: mc('grey', '50');
+        background-image: linear-gradient(to bottom, #FFF, mc('grey', '50'));
+        border-right: 1px solid mc('grey', '200');
+        border-bottom: 1px solid mc('grey', '200');
+        border-left: 5px solid mc('grey', '300');
+        box-shadow: 0 3px 8px 0 rgba(116, 129, 141, 0.1);
+        padding: 1rem;
+        border-radius: 5px;
+        font-weight: 500;
+
+        &:hover {
+          background-image: linear-gradient(to bottom, #FFF, lighten(mc('blue', '50'), 4%));
+          border-left-color: mc('blue', '500');
+          cursor: pointer;
+        }
+
+        &::before {
+          content: '';
+          display: none;
+        }
+
+        > a {
+          display: block;
+          text-decoration: none;
+          margin: -1rem;
+          padding: 1rem;
+        }
+
+        @at-root .theme--dark & {
+          background-color: mc('grey', '50');
+          background-image: linear-gradient(to bottom, lighten(mc('grey', '900'), 5%), mc('grey', '900'));
+          border-right: 1px solid mc('grey', '900');
+          border-bottom: 1px solid mc('grey', '900');
+          border-left: 5px solid mc('grey', '700');
+          box-shadow: 0 3px 8px 0 rgba(0, 0, 0, 0.1);
+
+          &:hover {
+            background-image: linear-gradient(to bottom, lighten(mc('grey', '900'), 2%), darken(mc('grey', '900'), 3%));
+            border-left-color: mc('blue', '500');
+            cursor: pointer;
+          }
+        }
+      }
+    }
+
+    &.grid-list {
+      margin: 1rem 24px 0 24px;
+      background-color: #FFF;
+      border: 1px solid mc('grey', '200');
+      padding: 1px;
+      display: inline-block;
+
+      @at-root .theme--dark & {
+        background-color: #000;
+        border: 1px solid mc('grey', '800');
+      }
+
+      li {
+        background-color: mc('grey', '50');
+        padding: .6rem 1rem;
+        display: block;
+
+        &:nth-child(odd) {
+          background-color: mc('grey', '100');
+        }
+
+        & + li {
+          margin-top: 0;
+        }
+
+        &::before {
+          color: mc('grey', '400');
+        }
+
+        @at-root .theme--dark & {
+          background-color: mc('grey', '900');
+
+          &:nth-child(odd) {
+            background-color: darken(mc('grey', '900'), 5%);
+          }
+        }
+      }
+    }
   }
 
   ul {
@@ -264,6 +350,11 @@
     &::before, &::after {
       display: none;
     }
+
+    @at-root .theme--dark & {
+      background-color: darken(mc('grey', '900'), 5%);
+      color: mc('indigo', '100');
+    }
   }
 
   .prismjs{

+ 1 - 0
server/models/assets.js

@@ -77,6 +77,7 @@ module.exports = class Asset extends Model {
       kind: _.startsWith(opts.mimetype, 'image/') ? 'image' : 'binary',
       mime: opts.mimetype,
       fileSize: opts.size,
+      folderId: null,
       authorId: opts.userId
     })