Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
W
wiki-js
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
1
Issues
1
List
Board
Labels
Milestones
Merge Requests
1
Merge Requests
1
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Registry
Registry
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Jacklull
wiki-js
Commits
6c12061c
Commit
6c12061c
authored
May 25, 2019
by
Nick
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: asset rename + delete
parent
e8b738aa
Hide whitespace changes
Inline
Side-by-side
Showing
13 changed files
with
519 additions
and
100 deletions
+519
-100
admin.vue
client/components/admin.vue
+9
-5
admin-general.vue
client/components/admin/admin-general.vue
+77
-55
admin-webhooks.vue
client/components/admin/admin-webhooks.vue
+116
-0
nav-header.vue
client/components/common/nav-header.vue
+4
-4
editor-modal-media.vue
client/components/editor/editor-modal-media.vue
+118
-8
editor-media-mutation-asset-delete.gql
client/graph/editor/editor-media-mutation-asset-delete.gql
+12
-0
editor-media-mutation-asset-rename.gql
client/graph/editor/editor-media-mutation-asset-rename.gql
+12
-0
v-dialog.scss
client/scss/components/v-dialog.scss
+6
-0
icon-winter.svg
client/static/svg/icon-winter.svg
+14
-0
asset.js
server/graph/resolvers/asset.js
+98
-28
asset.graphql
server/graph/schemas/asset.graphql
+9
-0
error.js
server/helpers/error.js
+32
-0
assets.js
server/models/assets.js
+12
-0
No files found.
client/components/admin.vue
View file @
6c12061c
...
...
@@ -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'
)
},
...
...
client/components/admin/admin-general.vue
View file @
6c12061c
...
...
@@ -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-p
2
s
v-card.wiki-form.animated.fadeInUp.wait-p
4
s
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
}
}
},
...
...
client/components/admin/admin-webhooks.vue
0 → 100644
View file @
6c12061c
<
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
>
client/components/common/nav-header.vue
View file @
6c12061c
...
...
@@ -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 & Files
v-list-tile-avatar: v-icon(color='
grey lighten-2
') burst_mode
v-list-tile-content
.grey--text.text--ligten-2
Images & 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')
...
...
client/components/editor/editor-modal-media.vue
View file @
6c12061c
...
...
@@ -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
:
{
...
...
client/graph/editor/editor-media-mutation-asset-delete.gql
0 → 100644
View file @
6c12061c
mutation
(
$id
:
Int
!)
{
assets
{
deleteAsset
(
id
:
$id
)
{
responseResult
{
succeeded
errorCode
slug
message
}
}
}
}
client/graph/editor/editor-media-mutation-asset-rename.gql
0 → 100644
View file @
6c12061c
mutation
(
$id
:
Int
!,
$filename
:
String
!)
{
assets
{
renameAsset
(
id
:
$id
,
filename
:
$filename
)
{
responseResult
{
succeeded
errorCode
slug
message
}
}
}
}
client/scss/components/v-dialog.scss
View file @
6c12061c
...
...
@@ -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'
))
,
...
...
client/static/svg/icon-winter.svg
0 → 100644
View file @
6c12061c
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 20.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns=
"http://www.w3.org/2000/svg"
xmlns:xlink=
"http://www.w3.org/1999/xlink"
version=
"1.1"
id=
"Слой_1"
x=
"0px"
y=
"0px"
viewBox=
"0 0 64 64"
style=
"enable-background:new 0 0 64 64;"
xml:space=
"preserve"
width=
"96px"
height=
"96px"
>
<linearGradient
id=
"SVGID_1__48266"
gradientUnits=
"userSpaceOnUse"
x1=
"32"
y1=
"5.3331"
x2=
"32"
y2=
"59.1757"
spreadMethod=
"reflect"
>
<stop
offset=
"0"
style=
"stop-color:#1A6DFF"
/>
<stop
offset=
"1"
style=
"stop-color:#C822FF"
/>
</linearGradient>
<path
style=
"fill:url(#SVGID_1__48266);"
d=
"M57.402,30.556l-2.959-2.959c-0.796-0.795-2.09-0.795-2.886,0l-2.96,2.96 C48.463,30.691,48.361,31,48.272,31h-7.137c-0.128,0-0.323-0.704-0.594-1.005l-2.404-2.671L42.46,23H47v-2h-4v-4h-2v4.632 l-4.203,4.203l-2.569-2.854C33.893,22.609,33,22.361,33,22.208v-6.48c0-0.089,0.309-0.192,0.443-0.326l2.96-2.96 c0.793-0.795,0.793-2.09-0.001-2.886l-2.959-2.959c-0.796-0.795-2.09-0.795-2.886,0l-2.96,2.96c-0.793,0.795-0.793,2.09,0.001,2.886 l2.959,2.959C30.691,15.536,31,15.639,31,15.728v6.48c0,0.152-0.893,0.401-1.228,0.772l-2.525,2.806L23,21.539V17h-2v4h-4v2h4.632 l4.275,4.275l-2.448,2.72C23.188,30.296,22.993,31,22.865,31h-7.137c-0.089,0-0.192-0.309-0.325-0.443l-2.959-2.959 c-0.796-0.795-2.09-0.795-2.886,0l-2.96,2.96c-0.793,0.795-0.793,2.09,0.001,2.886l2.959,2.959c0.398,0.397,0.92,0.597,1.443,0.597 s1.045-0.199,1.443-0.597l2.96-2.96C15.537,33.308,15.639,33,15.728,33h7.137c0.128,0,0.323,0.703,0.594,1.005l1.955,2.173L20.63,41 H16v2h4v4h2v-4.541l4.755-4.792l3.017,3.353C30.108,41.392,31,41.657,31,41.823v6.45c0,0.088-0.309,0.191-0.443,0.325l-2.96,2.96 c-0.793,0.795-0.793,2.09,0.001,2.886l2.959,2.959c0.398,0.397,0.92,0.597,1.443,0.597s1.045-0.199,1.443-0.597l2.96-2.96 c0.793-0.795,0.793-2.09-0.001-2.886l-2.959-2.959C33.309,48.464,33,48.361,33,48.272v-6.45c0-0.166,0.892-0.43,1.228-0.803 l3.024-3.36L42,42.371V47h2v-4h4v-2h-4.541l-4.867-4.83l1.949-2.165C40.812,33.704,41.007,33,41.135,33h7.137 c0.089,0,0.192,0.309,0.325,0.443l2.959,2.959c0.398,0.397,0.92,0.597,1.443,0.597s1.045-0.199,1.443-0.597l2.96-2.96 C58.197,32.647,58.197,31.352,57.402,30.556z M29.012,10.97l3.017-2.959l2.958,2.958c0.017,0.017,0.017,0.043,0,0.06l-3.017,2.959 L29.012,10.97z M10.971,34.988L8.012,31.97l3.017-2.959l2.958,2.958c0.017,0.017,0.017,0.043,0,0.06L10.971,34.988z M34.987,52.969 c0.017,0.017,0.017,0.043,0,0.06l-3.017,2.959l-2.959-3.018l3.017-2.959L34.987,52.969z M39.054,32.667l-6.313,7.015 c-0.383,0.426-1.101,0.426-1.483,0l-6.313-7.015c-0.342-0.38-0.342-0.954,0-1.334l6.313-7.015c0.191-0.213,0.455-0.329,0.742-0.329 s0.55,0.116,0.742,0.329l6.313,7.015C39.396,31.713,39.396,32.287,39.054,32.667z M52.971,34.988l-2.959-3.018l3.017-2.959 l2.958,2.958c0.017,0.017,0.017,0.043,0,0.06L52.971,34.988z"
/>
<linearGradient
id=
"SVGID_2__48266"
gradientUnits=
"userSpaceOnUse"
x1=
"32"
y1=
"25.9998"
x2=
"32"
y2=
"38.5098"
spreadMethod=
"reflect"
>
<stop
offset=
"0"
style=
"stop-color:#6DC7FF"
/>
<stop
offset=
"1"
style=
"stop-color:#E6ABFF"
/>
</linearGradient>
<path
style=
"fill:url(#SVGID_2__48266);"
d=
"M36.627,32.781l-3.746,4.162c-0.471,0.523-1.291,0.523-1.762,0l-3.746-4.162 c-0.406-0.451-0.406-1.135,0-1.586l3.746-4.162c0.471-0.523,1.291-0.523,1.762,0l3.746,4.162 C37.033,31.646,37.033,32.33,36.627,32.781z"
/>
</svg>
server/graph/resolvers/asset.js
View file @
6c12061c
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) {
...
...
server/graph/schemas/asset.graphql
View file @
6c12061c
...
...
@@ -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
"
])
}
# -----------------------------------------------
...
...
server/helpers/error.js
View file @
6c12061c
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
...
...
server/models/assets.js
View file @
6c12061c
...
...
@@ -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
)
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment