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

pull/861/head
Nick 6 years ago
parent 965f0ad2cd
commit 7b08c8bb31

@ -396,6 +396,13 @@ export default {
} }
return lvl 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 * Insert content after current line
*/ */
@ -457,6 +464,25 @@ export default {
toggleFullscreen () { toggleFullscreen () {
this.cm.setOption('fullScreen', true) 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> </script>

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

@ -11,9 +11,30 @@
v-btn.ml-3.my-0.radius-7(outline, large, color='teal', disabled, :icon='$vuetify.breakpoint.xsOnly') 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 v-icon(:left='$vuetify.breakpoint.mdAndUp') keyboard_arrow_up
span.hidden-sm-and-down Parent Folder span.hidden-sm-and-down Parent Folder
v-btn.my-0.mr-0.radius-7(outline, large, color='teal', :icon='$vuetify.breakpoint.xsOnly') v-dialog(v-model='newFolderDialog', max-width='550')
v-icon(:left='$vuetify.breakpoint.mdAndUp') add v-btn.my-0.mr-0.radius-7(outline, large, color='teal', :icon='$vuetify.breakpoint.xsOnly', slot='activator')
span.hidden-sm-and-down New Folder 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( v-data-table(
:items='assets' :items='assets'
:headers='headers' :headers='headers'
@ -26,40 +47,40 @@
template(slot='items', slot-scope='props') template(slot='items', slot-scope='props')
tr.is-clickable( tr.is-clickable(
@click.left='currentFileId = props.item.id' @click.left='currentFileId = props.item.id'
@click.right='' @click.right.prevent=''
:class='currentFileId === props.item.id ? `teal lighten-5` : ``' :class='currentFileId === props.item.id ? `teal lighten-5` : ``'
) )
td.text-xs-right(v-if='$vuetify.breakpoint.smAndUp') {{ props.item.id }} td.text-xs-right(v-if='$vuetify.breakpoint.smAndUp') {{ props.item.id }}
td td
.body-2(:class='currentFileId === props.item.id ? `teal--text` : ``') {{ props.item.filename }} .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') 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)}} .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.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') td(v-if='$vuetify.breakpoint.smAndUp')
v-menu(offset-x) 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-icon(color='grey darken-2') more_horiz
v-list.py-0 v-list.py-0
v-list-tile v-list-tile(@click='')
v-list-tile-avatar v-list-tile-avatar
v-icon(color='teal') short_text v-icon(color='teal') short_text
v-list-tile-content Properties v-list-tile-content Properties
v-divider v-divider
template(v-if='props.item.kind === `IMAGE`') template(v-if='props.item.kind === `IMAGE`')
v-list-tile v-list-tile(@click='')
v-list-tile-avatar v-list-tile-avatar
v-icon(color='indigo') crop_rotate v-icon(color='indigo') crop_rotate
v-list-tile-content Edit v-list-tile-content Edit
v-divider v-divider
v-list-tile v-list-tile(@click='')
v-list-tile-avatar v-list-tile-avatar
v-icon(color='blue') keyboard v-icon(color='blue') keyboard
v-list-tile-content Rename / Move v-list-tile-content Rename / Move
v-divider v-divider
v-list-tile v-list-tile(@click='')
v-list-tile-avatar v-list-tile-avatar
v-icon(color='red') delete v-icon(color='red') delete
v-list-tile-content 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' import listAssetQuery from 'gql/editor/editor-media-query-list.gql'
const FilePond = vueFilePond() const FilePond = vueFilePond()
const localeSegmentRegex = /^[A-Z]{2}(-[A-Z]{2})?$/i
const disallowedFolderChars = /[A-Z()=.!@#$%?&*+`~<>,;:\\/[\]¬{| ]/
export default { export default {
components: { components: {
@ -172,7 +195,9 @@ export default {
], ],
imageAlignment: '', imageAlignment: '',
currentFileId: null, currentFileId: null,
loading: false loading: false,
newFolderDialog: false,
newFolderName: ''
} }
}, },
computed: { computed: {
@ -191,12 +216,24 @@ export default {
headers() { headers() {
return _.compact([ return _.compact([
this.$vuetify.breakpoint.smAndUp && { text: 'ID', value: 'id', width: 50, align: 'right' }, this.$vuetify.breakpoint.smAndUp && { text: 'ID', value: 'id', width: 50, align: 'right' },
{ text: 'Title', value: 'title' }, { text: 'Filename', value: 'filename' },
this.$vuetify.breakpoint.lgAndUp && { text: 'Type', value: 'path', width: 50 }, this.$vuetify.breakpoint.lgAndUp && { text: 'Type', value: 'ext', width: 50 },
this.$vuetify.breakpoint.mdAndUp && { text: 'File Size', value: 'createdAt', width: 150 }, this.$vuetify.breakpoint.mdAndUp && { text: 'File Size', value: 'fileSize', width: 110 },
this.$vuetify.breakpoint.mdAndUp && { text: 'Last Updated', value: 'updatedAt', width: 150 }, this.$vuetify.breakpoint.mdAndUp && { text: 'Added', value: 'createdAt', width: 150 },
this.$vuetify.breakpoint.smAndUp && { text: '', value: '', width: 50, sortable: false } 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: { filters: {
@ -227,8 +264,13 @@ export default {
}, },
methods: { methods: {
insert () { insert () {
const asset = _.find(this.assets, ['id', this.currentFileId])
this.$root.$emit('editorInsert', {
kind: asset.kind,
path: `/${asset.filename}`,
text: asset.filename
})
this.activeModal = '' this.activeModal = ''
}, },
browse () { browse () {
this.$refs.pond.browse() this.$refs.pond.browse()
@ -262,6 +304,9 @@ export default {
}, 5000) }, 5000)
await this.$apollo.queries.assets.refetch() await this.$apollo.queries.assets.refetch()
},
async createFolder() {
} }
}, },
apollo: { apollo: {

@ -26,6 +26,12 @@
background-image: radial-gradient(ellipse at top, mc('grey', '800'), mc('grey', '900')), background-image: radial-gradient(ellipse at top, mc('grey', '800'), mc('grey', '900')),
radial-gradient(ellipse at bottom, 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 { .v-dialog--fullscreen {

@ -235,6 +235,92 @@
li + li { li + li {
margin-top: .5rem; 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 { ul {
@ -264,6 +350,11 @@
&::before, &::after { &::before, &::after {
display: none; display: none;
} }
@at-root .theme--dark & {
background-color: darken(mc('grey', '900'), 5%);
color: mc('indigo', '100');
}
} }
.prismjs{ .prismjs{

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

Loading…
Cancel
Save