Selaa lähdekoodia

feat: asset rename + delete

Nick 6 vuotta sitten
vanhempi
sitoutus
6c12061c17

+ 9 - 5
client/components/admin.vue

@@ -54,10 +54,10 @@
               v-list-tile-avatar: v-icon lock_outline
               v-list-tile-title {{ $t('admin:auth.title') }}
             v-list-tile(to='/editor', disabled)
-              v-list-tile-avatar: v-icon transform
+              v-list-tile-avatar: v-icon(color='grey lighten-2') transform
               v-list-tile-title {{ $t('admin:editor.title') }}
-            v-list-tile(to='/logging')
-              v-list-tile-avatar: v-icon graphic_eq
+            v-list-tile(to='/logging', disabled)
+              v-list-tile-avatar: v-icon(color='grey lighten-2') graphic_eq
               v-list-tile-title {{ $t('admin:logging.title') }}
             v-list-tile(to='/rendering')
               v-list-tile-avatar: v-icon system_update_alt
@@ -71,8 +71,8 @@
           template(v-if='hasPermission([`manage:system`, `manage:api`])')
             v-divider.my-2
             v-subheader.pl-4 {{ $t('admin:nav.system') }}
-            v-list-tile(to='/api', v-if='hasPermission([`manage:system`, `manage:api`])')
-              v-list-tile-avatar: v-icon call_split
+            v-list-tile(to='/api', v-if='hasPermission([`manage:system`, `manage:api`])', disabled)
+              v-list-tile-avatar: v-icon(color='grey lighten-2') call_split
               v-list-tile-title {{ $t('admin:api.title') }}
             v-list-tile(to='/mail', v-if='hasPermission(`manage:system`)')
               v-list-tile-avatar: v-icon email
@@ -83,6 +83,9 @@
             v-list-tile(to='/utilities', v-if='hasPermission(`manage:system`)', disabled)
               v-list-tile-avatar: v-icon(color='grey lighten-2') build
               v-list-tile-title {{ $t('admin:utilities.title') }}
+            v-list-tile(to='/webhooks', v-if='hasPermission(`manage:system`)', disabled)
+              v-list-tile-avatar: v-icon(color='grey lighten-2') ac_unit
+              v-list-tile-title {{ $t('admin:webhooks.title') }}
             v-list-group(
               to='/dev'
               no-action
@@ -150,6 +153,7 @@ const router = new VueRouter({
     { path: '/mail', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-mail.vue') },
     { path: '/system', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-system.vue') },
     { path: '/utilities', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-utilities.vue') },
+    { path: '/webhooks', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-webhooks.vue') },
     { path: '/dev-flags', component: () => import(/* webpackChunkName: "admin-dev" */ './admin/admin-dev-flags.vue') },
     { path: '/dev-graphiql', component: () => import(/* webpackChunkName: "admin-dev" */ './admin/admin-dev-graphiql.vue') },
     { path: '/dev-voyager', component: () => import(/* webpackChunkName: "admin-dev" */ './admin/admin-dev-voyager.vue') },

+ 77 - 55
client/components/admin/admin-general.vue

@@ -42,6 +42,32 @@
                       persistent-hint
                       )
                   v-divider
+                  v-subheader Logo #[v-chip.ml-2(label, color='grey', small, outline) coming soon]
+                  v-card-text.pb-4.pl-5
+                    v-layout.px-3(row, align-center)
+                      v-avatar(size='100', :color='$vuetify.dark ? `grey darken-2` : `grey lighten-3`', :tile='config.logoIsSquare')
+                      .ml-4
+                        v-btn.mx-0(color='teal', depressed, disabled)
+                          v-icon(left) cloud_upload
+                          span Upload Logo
+                        v-btn(color='teal', depressed, disabled)
+                          v-icon(left) clear
+                          span Clear
+                        .caption.grey--text An image of 120x120 pixels is recommended for best results.
+                        .caption.grey--text SVG, PNG or JPG files only.
+                  v-divider
+                  v-subheader Footer Copyright
+                  .px-3.pb-3
+                    v-text-field(
+                      outline
+                      label='Company / Organization Name'
+                      v-model='config.company'
+                      :counter='255'
+                      prepend-icon='business'
+                      persistent-hint
+                      hint='Name to use when displaying copyright notice in the footer. Leave empty to hide.'
+                      )
+                  v-divider
                   v-subheader SEO
                   .px-3.pb-3
                     v-text-field(
@@ -64,73 +90,65 @@
                       hint='Default: Index, Follow. Can also be set on a per-page basis.'
                       persistent-hint
                       )
-                  v-divider
-                  v-subheader Analytics #[v-chip.ml-2(label, color='grey', small, outline) coming soon]
-                  .px-3.pb-3
-                    v-select.mt-2(
-                      outline
-                      label='Analytics Service Provider'
-                      :items='analyticsServices'
-                      v-model='config.analyticsService'
-                      prepend-icon='timeline'
-                      persistent-hint
-                      hint='Automatically add tracking code for services like Google Analytics.'
-                      )
-                    v-text-field.mt-2(
-                      v-if='config.analyticsService !== ``'
-                      outline
-                      label='Property Tracking ID'
-                      :counter='255'
-                      v-model='config.analyticsId'
-                      prepend-icon='timeline'
-                      persistent-hint
-                      hint='A unique identifier provided by your analytics service provider.'
-                      )
+
             v-flex(lg6 xs12)
-              v-card.wiki-form.animated.fadeInUp.wait-p2s
+              v-card.wiki-form.animated.fadeInUp.wait-p4s
                 v-toolbar(color='primary', dark, dense, flat)
                   v-toolbar-title
-                    .subheading {{ $t('admin:general.siteBranding') }}
-                v-subheader Logo #[v-chip.ml-2(label, color='grey', small, outline) coming soon]
+                    .subheading Features
+                  v-spacer
+                  v-chip(label, color='white', small).primary--text coming soon
                 v-card-text
-                  v-layout.px-3(row, align-center)
-                    v-avatar(size='120', :color='$vuetify.dark ? `grey darken-2` : `grey lighten-3`', :tile='config.logoIsSquare')
-                    .ml-4
-                      v-btn.mx-0(color='teal', depressed, disabled)
-                        v-icon(left) cloud_upload
-                        span Upload Logo
-                      v-btn(color='teal', depressed, disabled)
-                        v-icon(left) clear
-                        span Clear
-                      .caption.grey--text An image of 120x120 pixels is recommended for best results.
-                      .caption.grey--text SVG, PNG or JPG files only.
                   v-switch(
-                    v-model='config.logoIsSquare'
-                    label='Use Square Logo Frame'
+                    label='Analytics'
                     color='primary'
+                    v-model='config.featureAnalytics'
                     persistent-hint
-                    hint='Check this option if a round logo frame doesn\'t work with your logo.'
+                    hint='Enable site analytics using service provider.'
+                    disabled
                     )
-                v-divider
-                v-subheader Footer Copyright
-                .px-3.pb-3
-                  v-text-field(
+                  v-select.mt-3(
                     outline
-                    label='Company / Organization Name'
-                    v-model='config.company'
+                    label='Analytics Service Provider'
+                    :items='analyticsServices'
+                    v-model='config.analyticsService'
+                    prepend-icon='subdirectory_arrow_right'
+                    persistent-hint
+                    hint='Automatically add tracking code for services like Google Analytics.'
+                    disabled
+                    )
+                  v-text-field.mt-2(
+                    v-if='config.analyticsService !== ``'
+                    outline
+                    label='Property Tracking ID'
                     :counter='255'
-                    prepend-icon='business'
+                    v-model='config.analyticsId'
+                    prepend-icon='timeline'
                     persistent-hint
-                    hint='Name to use when displaying copyright notice in the footer. Leave empty to hide.'
+                    hint='A unique identifier provided by your analytics service provider.'
                     )
 
-              v-card.wiki-form.mt-3.animated.fadeInUp.wait-p4s
-                v-toolbar(color='primary', dark, dense, flat)
-                  v-toolbar-title
-                    .subheading Features
-                  v-spacer
-                  v-chip(label, color='white', small).primary--text coming soon
-                v-card-text
+                  v-divider.mt-3
+                  v-switch(
+                    label='Asset Image Optimization'
+                    color='primary'
+                    v-model='config.featureTinyPNG'
+                    persistent-hint
+                    hint='Image optimization tool to reduce filesize and bandwidth costs.'
+                    disabled
+                    )
+                  v-text-field.mt-3(
+                    outline
+                    label='TinyPNG API Key'
+                    :counter='255'
+                    v-model='config.description'
+                    prepend-icon='subdirectory_arrow_right'
+                    hint='Get your API key at https://tinypng.com/developers'
+                    persistent-hint
+                    disabled
+                    )
+
+                  v-divider.mt-3
                   v-switch(
                     label='Page Ratings'
                     color='primary'
@@ -138,6 +156,7 @@
                     persistent-hint
                     hint='Allow users to rate pages.'
                     )
+
                   v-divider.mt-3
                   v-switch(
                     label='Page Comments'
@@ -146,6 +165,7 @@
                     persistent-hint
                     hint='Allow users to leave comments on pages.'
                     )
+
                   v-divider.mt-3
                   v-switch(
                     label='Personal Wikis'
@@ -188,9 +208,11 @@ export default {
         company: '',
         hasLogo: false,
         logoIsSquare: false,
+        featureAnalytics: false,
         featurePageRatings: false,
         featurePageComments: false,
-        featurePersonalWikis: false
+        featurePersonalWikis: false,
+        featureTinyPNG: false
       }
     }
   },

+ 116 - 0
client/components/admin/admin-webhooks.vue

@@ -0,0 +1,116 @@
+<template lang='pug'>
+  v-container(fluid, grid-list-lg)
+    v-layout(row, wrap)
+      v-flex(xs12)
+        .admin-header
+          img.animated.fadeInUp(src='/svg/icon-winter.svg', alt='Mail', style='width: 80px;')
+          .admin-header-title
+            .headline.primary--text.animated.fadeInLeft {{ $t('admin:webhooks.title') }}
+            .subheading.grey--text.animated.fadeInLeft.wait-p4s {{ $t('admin:webhooks.subtitle') }}
+          v-spacer
+          v-btn.animated.fadeInDown(color='success', depressed, @click='save', large, disabled)
+            v-icon(left) check
+            span {{$t('common:actions.apply')}}
+
+      v-flex(lg3, xs12)
+        v-card.animated.fadeInUp
+          v-toolbar(flat, color='primary', dark, dense)
+            .subheading Webhooks
+            v-spacer
+            v-btn(outline, small)
+              v-icon.mr-2 add
+              span New
+          v-list(two-line, dense).py-0
+            template(v-for='(str, idx) in hooks')
+              v-list-tile(:key='str.key', @click='selectedHook = str.key')
+                v-list-tile-avatar
+                  v-icon(color='primary', v-if='str.isEnabled', v-ripple, @click='str.isEnabled = false') check_box
+                  v-icon(color='grey', v-else, v-ripple, @click='str.isEnabled = true') check_box_outline_blank
+                v-list-tile-content
+                  v-list-tile-title.body-2(:class='!str.isAvailable ? `grey--text` : (selectedHook === str.key ? `primary--text` : ``)') {{ str.title }}
+                  v-list-tile-sub-title.caption(:class='!str.isAvailable ? `grey--text text--lighten-1` : (selectedHook === str.key ? `blue--text ` : ``)') {{ str.description }}
+                v-list-tile-avatar(v-if='selectedHook === str.key')
+                  v-icon.animated.fadeInLeft(color='primary') arrow_forward_ios
+              v-divider(v-if='idx < hooks.length - 1')
+
+      v-flex(xs12, lg9)
+        v-card.wiki-form.animated.fadeInUp.wait-p2s
+          v-toolbar(color='primary', dense, flat, dark)
+            .subheading {{hook.title}}
+          v-card-text
+            v-form
+              .authlogo
+                img(:src='hook.logo', :alt='hook.title')
+              .caption.pt-3 {{hook.description}}
+              .caption.pb-3: a(:href='hook.website') {{hook.website}}
+              .body-2(v-if='hook.isEnabled')
+                span This hook is
+
+</template>
+
+<script>
+import _ from 'lodash'
+import { get } from 'vuex-pathify'
+import mailConfigQuery from 'gql/admin/mail/mail-query-config.gql'
+import mailUpdateConfigMutation from 'gql/admin/mail/mail-mutation-save-config.gql'
+
+export default {
+  data() {
+    return {
+      hooks: [],
+      selectedHook: ''
+    }
+  },
+  computed: {
+    hook() {
+      return _.find(this.hooks, ['id', this.selectedHook]) || {}
+    }
+  },
+  methods: {
+    async save () {
+      try {
+        await this.$apollo.mutate({
+          mutation: mailUpdateConfigMutation,
+          variables: {
+            senderName: this.config.senderName || '',
+            senderEmail: this.config.senderEmail || '',
+            host: this.config.host || '',
+            port: _.toSafeInteger(this.config.port) || 0,
+            secure: this.config.secure || false,
+            user: this.config.user || '',
+            pass: this.config.pass || '',
+            useDKIM: this.config.useDKIM || false,
+            dkimDomainName: this.config.dkimDomainName || '',
+            dkimKeySelector: this.config.dkimKeySelector || '',
+            dkimPrivateKey: this.config.dkimPrivateKey || ''
+          },
+          watchLoading (isLoading) {
+            this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-mail-update')
+          }
+        })
+        this.$store.commit('showNotification', {
+          style: 'success',
+          message: 'Configuration saved successfully.',
+          icon: 'check'
+        })
+      } catch (err) {
+        this.$store.commit('pushGraphError', err)
+      }
+    }
+  },
+  apollo: {
+    hooks: {
+      query: mailConfigQuery,
+      fetchPolicy: 'network-only',
+      update: (data) => _.cloneDeep(data.mail.config),
+      watchLoading (isLoading) {
+        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-mail-refresh')
+      }
+    }
+  }
+}
+</script>
+
+<style lang='scss'>
+
+</style>

+ 4 - 4
client/components/common/nav-header.vue

@@ -45,16 +45,16 @@
                   v-list-tile-avatar: v-icon(color='indigo') code
                   v-list-tile-content View Source
                 v-list-tile(avatar, @click='pageMove')
-                  v-list-tile-avatar: v-icon(color='indigo') forward
-                  v-list-tile-content Move / Rename
+                  v-list-tile-avatar: v-icon(color='grey lighten-2') forward
+                  v-list-tile-content.grey--text.text--ligten-2 Move / Rename
                 v-list-tile(avatar, @click='pageDelete')
                   v-list-tile-avatar: v-icon(color='red darken-2') delete
                   v-list-tile-content Delete
               v-divider.my-0
               v-subheader Assets
               v-list-tile(avatar, @click='assets')
-                v-list-tile-avatar: v-icon(color='blue-grey') burst_mode
-                v-list-tile-content Images &amp; Files
+                v-list-tile-avatar: v-icon(color='grey lighten-2') burst_mode
+                v-list-tile-content.grey--text.text--ligten-2 Images &amp; Files
           v-toolbar-title(:class='{ "ml-2": $vuetify.breakpoint.mdAndUp, "ml-0": $vuetify.breakpoint.smAndDown }')
             span.subheading {{title}}
       v-flex(md4, v-if='$vuetify.breakpoint.mdAndUp')

+ 118 - 8
client/components/editor/editor-modal-media.vue

@@ -70,38 +70,38 @@
                         v-btn.ma-0(icon, slot='activator')
                           v-icon(color='grey darken-2') more_horiz
                         v-list.py-0(style='border-top: 5px solid #444;')
-                          v-list-tile(@click='')
+                          v-list-tile(@click='', disabled)
                             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(@click='')
+                            v-list-tile(@click='previewDialog = true', disabled)
                               v-list-tile-avatar
                                 v-icon(color='green') image_search
                               v-list-tile-content Preview
                             v-divider
-                            v-list-tile(@click='')
+                            v-list-tile(@click='', disabled)
                               v-list-tile-avatar
                                 v-icon(color='indigo') crop_rotate
                               v-list-tile-content Edit
                             v-divider
-                            v-list-tile(@click='')
+                            v-list-tile(@click='', disabled)
                               v-list-tile-avatar
                                 v-icon(color='purple') offline_bolt
                               v-list-tile-content Optimize
                             v-divider
-                          v-list-tile(@click='')
+                          v-list-tile(@click='openRenameDialog')
                             v-list-tile-avatar
                               v-icon(color='orange') keyboard
                             v-list-tile-content Rename
                           v-divider
-                          v-list-tile(@click='')
+                          v-list-tile(@click='', disabled)
                             v-list-tile-avatar
                               v-icon(color='blue') forward
                             v-list-tile-content Move
                           v-divider
-                          v-list-tile(@click='')
+                          v-list-tile(@click='deleteDialog = true')
                             v-list-tile-avatar
                               v-icon(color='red') delete
                             v-list-tile-content Delete
@@ -186,6 +186,44 @@
                 background-color='grey lighten-2'
                 placeholder='None'
               )
+
+    //- RENAME DIALOG
+
+    v-dialog(v-model='renameDialog', max-width='550', persistent)
+      v-card.wiki-form
+        .dialog-header.is-short.is-orange
+          v-icon.mr-2(color='white') keyboard
+          span Rename Asset
+        v-card-text
+          .body-2 Enter the new name for this asset:
+          v-text-field(
+            outline
+            single-line
+            :counter='255'
+            v-model='renameAssetName'
+            @keyup.enter='renameAsset'
+            :disabled='renameAssetLoading'
+          )
+        v-card-chin
+          v-spacer
+          v-btn(flat, @click='renameDialog = false', :disabled='renameAssetLoading') Cancel
+          v-btn(color='orange darken-3', @click='renameAsset', :loading='renameAssetLoading').white--text Rename
+
+    //- DELETE DIALOG
+
+    v-dialog(v-model='deleteDialog', max-width='550', persistent)
+      v-card.wiki-form
+        .dialog-header.is-short.is-red
+          v-icon.mr-2(color='white') highlight_off
+          span Delete Asset
+        v-card-text
+          .body-2 Are you sure you want to delete asset
+          .body-2.red--text.text--darken-2 {{currentAsset.filename}}?
+          .caption.mt-3 This action cannot be undone!
+        v-card-chin
+          v-spacer
+          v-btn(flat, @click='deleteDialog = false', :disabled='deleteAssetLoading') Cancel
+          v-btn(color='red darken-2', @click='deleteAsset', :loading='deleteAssetLoading').white--text Delete
 </template>
 
 <script>
@@ -197,6 +235,8 @@ import 'filepond/dist/filepond.min.css'
 import listAssetQuery from 'gql/editor/editor-media-query-list.gql'
 import listFolderAssetQuery from 'gql/editor/editor-media-query-folder-list.gql'
 import createAssetFolderMutation from 'gql/editor/editor-media-mutation-folder-create.gql'
+import renameAssetMutation from 'gql/editor/editor-media-mutation-asset-rename.gql'
+import deleteAssetMutation from 'gql/editor/editor-media-mutation-asset-delete.gql'
 
 const FilePond = vueFilePond()
 const localeSegmentRegex = /^[A-Z]{2}(-[A-Z]{2})?$/i
@@ -234,7 +274,13 @@ export default {
       loading: false,
       newFolderDialog: false,
       newFolderName: '',
-      newFolderLoading: false
+      newFolderLoading: false,
+      previewDialog: false,
+      renameDialog: false,
+      renameAssetName: '',
+      renameAssetLoading: false,
+      deleteDialog: false,
+      deleteAssetLoading: false
     }
   },
   computed: {
@@ -262,6 +308,9 @@ export default {
     },
     isFolderNameValid() {
       return this.newFolderName.length > 1 && !localeSegmentRegex.test(this.newFolderName) && !disallowedFolderChars.test(this.newFolderName)
+    },
+    currentAsset () {
+      return _.find(this.assets, ['id', this.currentFileId]) || {}
     }
   },
   watch: {
@@ -389,6 +438,67 @@ export default {
       }
       this.newFolderLoading = false
       this.$store.commit(`loadingStop`, 'editor-media-createfolder')
+    },
+    openRenameDialog() {
+      this.renameAssetName = this.currentAsset.filename
+      this.renameDialog = true
+    },
+    async renameAsset() {
+      this.$store.commit(`loadingStart`, 'editor-media-renameasset')
+      this.renameAssetLoading = true
+      try {
+        const resp = await this.$apollo.mutate({
+          mutation: renameAssetMutation,
+          variables: {
+            id: this.currentFileId,
+            filename: this.renameAssetName
+          }
+        })
+        if (_.get(resp, 'data.assets.renameAsset.responseResult.succeeded', false)) {
+          await this.$apollo.queries.assets.refetch()
+          this.$store.commit('showNotification', {
+            message: 'Asset renamed successfully.',
+            style: 'success',
+            icon: 'check'
+          })
+          this.renameDialog = false
+          this.renameAssetName = ''
+        } else {
+          this.$store.commit('pushGraphError', new Error(_.get(resp, 'data.assets.renameAsset.responseResult.message')))
+        }
+      } catch (err) {
+        this.$store.commit('pushGraphError', err)
+      }
+      this.renameAssetLoading = false
+      this.$store.commit(`loadingStop`, 'editor-media-renameasset')
+    },
+    async deleteAsset() {
+      this.$store.commit(`loadingStart`, 'editor-media-deleteasset')
+      this.deleteAssetLoading = true
+      try {
+        const resp = await this.$apollo.mutate({
+          mutation: deleteAssetMutation,
+          variables: {
+            id: this.currentFileId
+          }
+        })
+        if (_.get(resp, 'data.assets.deleteAsset.responseResult.succeeded', false)) {
+          this.currentFileId = null
+          await this.$apollo.queries.assets.refetch()
+          this.$store.commit('showNotification', {
+            message: 'Asset deleted successfully.',
+            style: 'success',
+            icon: 'check'
+          })
+          this.deleteDialog = false
+        } else {
+          this.$store.commit('pushGraphError', new Error(_.get(resp, 'data.assets.deleteAsset.responseResult.message')))
+        }
+      } catch (err) {
+        this.$store.commit('pushGraphError', err)
+      }
+      this.deleteAssetLoading = false
+      this.$store.commit(`loadingStop`, 'editor-media-deleteasset')
     }
   },
   apollo: {

+ 12 - 0
client/graph/editor/editor-media-mutation-asset-delete.gql

@@ -0,0 +1,12 @@
+mutation ($id: Int!) {
+  assets {
+    deleteAsset(id: $id) {
+      responseResult {
+        succeeded
+        errorCode
+        slug
+        message
+      }
+    }
+  }
+}

+ 12 - 0
client/graph/editor/editor-media-mutation-asset-rename.gql

@@ -0,0 +1,12 @@
+mutation ($id: Int!, $filename: String!) {
+  assets {
+    renameAsset(id:$id, filename: $filename) {
+      responseResult {
+        succeeded
+        errorCode
+        slug
+        message
+      }
+    }
+  }
+}

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

@@ -15,6 +15,12 @@
                       radial-gradient(ellipse at bottom, mc('red', '800'), mc('red', '700'));
   }
 
+  &.is-orange {
+    background-color: mc('orange', '700');
+    background-image: radial-gradient(ellipse at top, mc('orange', '600'), mc('orange', '800')),
+                      radial-gradient(ellipse at bottom, mc('orange', '900'), mc('orange', '800'));
+  }
+
   &.is-indigo {
     background-color: mc('indigo', '700');
     background-image: radial-gradient(ellipse at top, mc('indigo', '500'), mc('indigo', '700')),

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 7 - 0
client/static/svg/icon-winter.svg


+ 98 - 28
server/graph/resolvers/asset.js

@@ -1,5 +1,7 @@
+const _ = require('lodash')
 const sanitize = require('sanitize-filename')
 const graphHelper = require('../../helpers/graph')
+const assetHelper = require('../../helpers/asset')
 
 /* global WIKI */
 
@@ -33,6 +35,9 @@ module.exports = {
     }
   },
   AssetMutation: {
+    /**
+     * Create New Asset Folder
+     */
     async createFolder(obj, args, context) {
       try {
         const folderSlug = sanitize(args.slug).toLowerCase()
@@ -56,35 +61,100 @@ module.exports = {
       } catch (err) {
         return graphHelper.generateError(err)
       }
+    },
+    /**
+     * Rename an Asset
+     */
+    async renameAsset(obj, args, context) {
+      try {
+        const filename = sanitize(args.filename).toLowerCase()
+
+        const asset = await WIKI.models.assets.query().findById(args.id)
+        if (asset) {
+          // Check for extension mismatch
+          if (!_.endsWith(filename, asset.ext)) {
+            throw new WIKI.Error.AssetRenameInvalidExt()
+          }
+
+          // Check for non-dot files changing to dotfile
+          if (asset.ext.length > 0 && filename.length - asset.ext.length < 1) {
+            throw new WIKI.Error.AssetRenameInvalid()
+          }
+
+          // Check for collision
+          const assetCollision = await WIKI.models.assets.query().where({
+            filename,
+            folderId: asset.folderId
+          }).first()
+          if (assetCollision) {
+            throw new WIKI.Error.AssetRenameCollision()
+          }
+
+          // Get asset folder path
+          let hierarchy = []
+          if (asset.folderId) {
+            hierarchy = await WIKI.models.assetFolders.getHierarchy(asset.folderId)
+          }
+
+          // Check source asset permissions
+          const assetSourcePath = (asset.folderId) ? hierarchy.map(h => h.slug).join('/') + `/${filename}` : filename
+          if (!WIKI.auth.checkAccess(context.req.user, ['manage:assets'], { path: assetSourcePath })) {
+            throw new WIKI.Error.AssetRenameForbidden()
+          }
+
+          // Check target asset permissions
+          const assetTargetPath = (asset.folderId) ? hierarchy.map(h => h.slug).join('/') + `/${filename}` : filename
+          if (!WIKI.auth.checkAccess(context.req.user, ['write:assets'], { path: assetTargetPath })) {
+            throw new WIKI.Error.AssetRenameTargetForbidden()
+          }
+
+          // Update filename + hash
+          const fileHash = assetHelper.generateHash(assetTargetPath)
+          await WIKI.models.assets.query().patch({
+            filename: filename,
+            hash: fileHash
+          }).findById(args.id)
+
+          // Delete old asset cache
+          await asset.deleteAssetCache()
+
+          return {
+            responseResult: graphHelper.generateSuccess('Asset has been renamed successfully.')
+          }
+        } else {
+          throw new WIKI.Error.AssetInvalid()
+        }
+      } catch (err) {
+        return graphHelper.generateError(err)
+      }
+    },
+    /**
+     * Delete an Asset
+     */
+    async deleteAsset(obj, args, context) {
+      try {
+        const asset = await WIKI.models.assets.query().findById(args.id)
+        if (asset) {
+          // Check permissions
+          const assetPath = asset.getAssetPath()
+          if (!WIKI.auth.checkAccess(context.req.user, ['manage:assets'], { path: assetPath })) {
+            throw new WIKI.Error.AssetDeleteForbidden()
+          }
+
+          await WIKI.models.knex('assetData').where('id', args.id).del()
+          await WIKI.models.assets.query().deleteById(args.id)
+          await asset.deleteAssetCache()
+
+          return {
+            responseResult: graphHelper.generateSuccess('Asset has been deleted successfully.')
+          }
+        } else {
+          throw new WIKI.Error.AssetInvalid()
+        }
+      } catch (err) {
+        return graphHelper.generateError(err)
+      }
     }
-    // deleteFile(obj, args) {
-    //   return WIKI.models.File.destroy({
-    //     where: {
-    //       id: args.id
-    //     },
-    //     limit: 1
-    //   })
-    // },
-    // renameFile(obj, args) {
-    //   return WIKI.models.File.update({
-    //     filename: args.filename
-    //   }, {
-    //     where: { id: args.id }
-    //   })
-    // },
-    // moveFile(obj, args) {
-    //   return WIKI.models.File.findById(args.fileId).then(fl => {
-    //     if (!fl) {
-    //       throw new gql.GraphQLError('Invalid File ID')
-    //     }
-    //     return WIKI.models.Folder.findById(args.folderId).then(fld => {
-    //       if (!fld) {
-    //         throw new gql.GraphQLError('Invalid Folder ID')
-    //       }
-    //       return fl.setFolder(fld)
-    //     })
-    //   })
-    // }
   }
   // File: {
   //   folder(fl) {

+ 9 - 0
server/graph/schemas/asset.graphql

@@ -35,6 +35,15 @@ type AssetMutation {
     slug: String!
     name: String
   ): DefaultResponse @auth(requires: ["manage:system", "write:assets"])
+
+  renameAsset(
+    id: Int!
+    filename: String!
+  ): DefaultResponse @auth(requires: ["manage:system", "manage:assets"])
+
+  deleteAsset(
+    id: Int!
+  ): DefaultResponse @auth(requires: ["manage:system", "manage:assets"])
 }
 
 # -----------------------------------------------

+ 32 - 0
server/helpers/error.js

@@ -1,10 +1,42 @@
 const CustomError = require('custom-error-instance')
 
 module.exports = {
+  AssetDeleteForbidden: CustomError('AssetDeleteForbidden', {
+    message: 'You are not authorized to delete this asset.',
+    code: 2003
+  }),
   AssetFolderExists: CustomError('AssetFolderExists', {
     message: 'An asset folder with the same name already exists.',
+    code: 2002
+  }),
+  AssetGenericError: CustomError('AssetGenericError', {
+    message: 'An unexpected error occured during asset change.',
     code: 2001
   }),
+  AssetInvalid: CustomError('AssetInvalid', {
+    message: 'This asset does not exist or is invalid.',
+    code: 2004
+  }),
+  AssetRenameCollision: CustomError('AssetRenameCollision', {
+    message: 'An asset with the same filename in the same folder already exists.',
+    code: 2005
+  }),
+  AssetRenameForbidden: CustomError('AssetRenameForbidden', {
+    message: 'You are not authorized to rename this asset.',
+    code: 2006
+  }),
+  AssetRenameInvalid: CustomError('AssetRenameInvalid', {
+    message: 'The new asset filename is invalid.',
+    code: 2007
+  }),
+  AssetRenameInvalidExt: CustomError('AssetRenameInvalidExt', {
+    message: 'The file extension cannot be changed on an existing asset.',
+    code: 2008
+  }),
+  AssetRenameTargetForbidden: CustomError('AssetRenameTargetForbidden', {
+    message: 'You are not authorized to rename this asset to the requested name.',
+    code: 2009
+  }),
   AuthAccountBanned: CustomError('AuthAccountBanned', {
     message: 'Your account has been disabled.',
     code: 1016

+ 12 - 0
server/models/assets.js

@@ -65,6 +65,18 @@ module.exports = class Asset extends Model {
     this.updatedAt = moment.utc().toISOString()
   }
 
+  async getAssetPath() {
+    let hierarchy = []
+    if (this.folderId) {
+      hierarchy = await WIKI.models.assetFolders.getHierarchy(this.folderId)
+    }
+    return (this.folderId) ? hierarchy.map(h => h.slug).join('/') + `/${this.filename}` : this.filename
+  }
+
+  async deleteAssetCache() {
+    await fs.remove(path.join(process.cwd(), `data/cache/${this.hash}.dat`))
+  }
+
   static async upload(opts) {
     const fileInfo = path.parse(opts.originalname)
     const fileHash = assetHelper.generateHash(opts.assetPath)

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä