Unverified Commit b979e508 authored by NGPixel's avatar NGPixel

chore: purge legacy code

parent 9fb9f53d
{
"comments": true,
"plugins": [
"lodash",
"graphql-tag",
"@babel/plugin-syntax-dynamic-import",
"@babel/plugin-syntax-import-meta",
"@babel/plugin-proposal-class-properties",
"@babel/plugin-proposal-json-strings",
[
"@babel/plugin-proposal-decorators",
{
"legacy": true
}
],
"@babel/plugin-proposal-function-sent",
"@babel/plugin-proposal-export-namespace-from",
"@babel/plugin-proposal-numeric-separator",
"@babel/plugin-proposal-throw-expressions",
[
"prismjs", {
"languages": ["clike", "markup"],
"plugins": ["line-numbers", "autoloader", "normalize-whitespace", "copy-to-clipboard", "toolbar"],
"theme": "twilight",
"css": true
}
]
],
"presets": [
[
"@babel/preset-env", {
"useBuiltIns": "entry",
"corejs": 3,
"debug": false
}
]
]
}
...@@ -9,4 +9,11 @@ git config oh-my-zsh.hide-info 1 ...@@ -9,4 +9,11 @@ git config oh-my-zsh.hide-info 1
echo "Waiting for DB container to come online..." echo "Waiting for DB container to come online..."
/usr/local/bin/wait-for localhost:5432 -- echo "DB ready" /usr/local/bin/wait-for localhost:5432 -- echo "DB ready"
echo "Installing dependencies..."
cd server
npm install
cd ../ux
npm install
cd ..
echo "Ready!" echo "Ready!"
...@@ -33,7 +33,7 @@ ...@@ -33,7 +33,7 @@
"args": [], "args": [],
// Set the shell type // Set the shell type
"options": { "options": {
"cwd": "/workspace" "cwd": "/workspace/server"
}, },
// Mark as a background task to avoid the spinner animation on the terminal tab // Mark as a background task to avoid the spinner animation on the terminal tab
"isBackground": true, "isBackground": true,
......
...@@ -43,7 +43,6 @@ The current stable release (2.x) is available at https://js.wiki ...@@ -43,7 +43,6 @@ The current stable release (2.x) is available at https://js.wiki
### Requirements ### Requirements
- Node.js **18.x** or later - Node.js **18.x** or later
- Yarn
- PostgreSQL **11** or later - PostgreSQL **11** or later
### Usage ### Usage
...@@ -53,18 +52,18 @@ The current stable release (2.x) is available at https://js.wiki ...@@ -53,18 +52,18 @@ The current stable release (2.x) is available at https://js.wiki
1. Edit `config.yml` and fill in the database details. **You need an empty PostgreSQL database.** 1. Edit `config.yml` and fill in the database details. **You need an empty PostgreSQL database.**
1. Run the following commands to install dependencies and generate the client assets: 1. Run the following commands to install dependencies and generate the client assets:
```sh ```sh
yarn cd server
yarn legacy:build npm install
cd ux cd ../ux
yarn npm install
yarn build npm run build
cd .. cd ..
``` ```
1. Run this command to start the server: 1. Run this command to start the server:
```sh ```sh
node server node server
``` ```
1. In your browser, navigate to `http://localhost:5000` *(or the IP/hostname of the server and the PORT you defined earlier.)* 1. In your browser, navigate to `http://localhost:3000` *(or the IP/hostname of the server and the PORT you defined earlier.)*
1. Login using the default administrator user: 1. Login using the default administrator user:
- Email: `admin@example.com` - Email: `admin@example.com`
- Password: `12345678` - Password: `12345678`
...@@ -93,17 +92,13 @@ The current stable release (2.x) is available at https://js.wiki ...@@ -93,17 +92,13 @@ The current stable release (2.x) is available at https://js.wiki
1. Two terminals will launch in split-screen mode at the bottom of the screen. **Server** on the left and **UX** on the right. 1. Two terminals will launch in split-screen mode at the bottom of the screen. **Server** on the left and **UX** on the right.
1. In the left-side terminal (Server), run the command: 1. In the left-side terminal (Server), run the command:
```sh ```sh
yarn legacy:build npm run dev
``` ```
1. In the right-side terminal (UX), run the command: 1. In the right-side terminal (UX), run the command:
```sh ```sh
yarn build npm run dev
``` ```
1. Back in the left-side terminal (Server), run the command: 1. Open your browser to `http://localhost:3000`
```sh
yarn dev
```
1. Open your browser to `http://localhost:5000`
1. Login using the default administrator user: 1. Login using the default administrator user:
- Email: `admin@example.com` - Email: `admin@example.com`
- Password: `12345678` - Password: `12345678`
...@@ -115,7 +110,7 @@ The current stable release (2.x) is available at https://js.wiki ...@@ -115,7 +110,7 @@ The current stable release (2.x) is available at https://js.wiki
From the left-side terminal (Server), run the command: From the left-side terminal (Server), run the command:
```sh ```sh
yarn dev npm run dev
``` ```
This will launch the server and automatically restart upon modification of any server files. This will launch the server and automatically restart upon modification of any server files.
...@@ -124,31 +119,18 @@ Only precompiled client assets are served in this mode. See the sections below o ...@@ -124,31 +119,18 @@ Only precompiled client assets are served in this mode. See the sections below o
### Frontend Development (Quasar/Vue 3) ### Frontend Development (Quasar/Vue 3)
> Make sure you are running `yarn dev` in the left-side terminal (Server) first! Requests still need to be forwarded to the server, even in SPA mode! > Make sure you are running `npm run dev` in the left-side terminal (Server) first! Requests still need to be forwarded to the server, even in SPA mode!
If you wish to modify any frontend content (under `/ux`), you need to start the Quasar Dev Server in the right-side terminal (UX): If you wish to modify any frontend content (under `/ux`), you need to start the Quasar Dev Server in the right-side terminal (UX):
```sh ```sh
yarn dev npm run dev
``` ```
You can then access the site at `http://localhost:5001`. Notice the port being `5001` rather than `5000`. The app runs in a SPA (single-page application) mode and automatically hot-reload any modified component. Any requests made to the `/graphql` endpoint are automatically forwarded to the server running on port `5000`, which is why both must be running at the same time. You can then access the site at `http://localhost:5001`. Notice the port being `5001` rather than `5000`. The app runs in a SPA (single-page application) mode and automatically hot-reload any modified component. Any requests made to the `/graphql` endpoint are automatically forwarded to the server running on port `5000`, which is why both must be running at the same time.
Note that not all sections/features are available from this mode, notably the page editing features which still relies on the old client code (Vuetify/Vue 2). For example, trying to edit a page will simply not work. You must use the normal mode (port 5000) to edit pages as it relies on legacy client code. As more features gets ported / developed for Vue 3, they will become available in the SPA mode.
Any change you make to the frontend will not be reflected on port 5000 until you run the command `yarn build` in the right-side terminal. Any change you make to the frontend will not be reflected on port 5000 until you run the command `yarn build` in the right-side terminal.
### Legacy Frontend Development (Vuetify/Vue 2)
Client code from Wiki.js 2.x is located under `/client`. Some sections still rely on this legacy code (notably the page editing features). Code is gradually being removed from this location and replaced with newer code in `/ux`.
In the unlikely event that you need to modify legacy code and regenerate the old client files, you can do so by running in this command in the left-side terminal (Server):
```sh
yarn legacy:build
```
Then run `yarn dev` to start the server again.
### pgAdmin ### pgAdmin
A web version of pgAdmin (a PostgreSQL administration tool) is available at `http://localhost:8000`. Use the login `dev` / `123123` to login. A web version of pgAdmin (a PostgreSQL administration tool) is available at `http://localhost:8000`. Use the login `dev` / `123123` to login.
......
...@@ -8,6 +8,7 @@ If you find such vulnerability, it's important to disclose it in a quick and sec ...@@ -8,6 +8,7 @@ If you find such vulnerability, it's important to disclose it in a quick and sec
| Version | Supported | | Version | Supported |
| ------- | ------------------ | | ------- | ------------------ |
| 3.x.x | :white_check_mark: |
| 2.x.x | :white_check_mark: | | 2.x.x | :white_check_mark: |
| 1.x.x | :x: | | 1.x.x | :x: |
......
module.exports = {
classPrefix: 'mdz-',
options: ['setClasses'],
'feature-detects': [
'css/backdropfilter'
]
}
/* global siteConfig */
import Vue from 'vue'
import VueRouter from 'vue-router'
import VueClipboards from 'vue-clipboards'
import { ApolloClient } from 'apollo-client'
import { BatchHttpLink } from 'apollo-link-batch-http'
import { ApolloLink } from 'apollo-link'
import { ErrorLink } from 'apollo-link-error'
import { InMemoryCache } from 'apollo-cache-inmemory'
import VueApollo from 'vue-apollo'
import Vuetify from 'vuetify/lib'
import Velocity from 'velocity-animate'
import Vuescroll from 'vuescroll/dist/vuescroll-native'
import Hammer from 'hammerjs'
import moment from 'moment-timezone'
import VueMoment from 'vue-moment'
import store from './store'
import Cookies from 'js-cookie'
// ====================================
// Load Modules
// ====================================
import boot from './modules/boot'
import localization from './modules/localization'
// ====================================
// Load Helpers
// ====================================
import helpers from './helpers'
// ====================================
// Initialize Global Vars
// ====================================
window.WIKI = null
window.boot = boot
window.Hammer = Hammer
moment.locale(siteConfig.lang)
store.commit('user/REFRESH_AUTH')
// ====================================
// Initialize Apollo Client (GraphQL)
// ====================================
const graphQLEndpoint = window.location.protocol + '//' + window.location.host + '/_graphql'
const graphQLLink = ApolloLink.from([
new ErrorLink(({ graphQLErrors, networkError }) => {
if (graphQLErrors) {
let isAuthError = false
graphQLErrors.map(({ message, locations, path }) => {
if (message === `Forbidden`) {
isAuthError = true
}
console.error(`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`)
})
store.commit('showNotification', {
style: 'red',
message: isAuthError ? `You are not authorized to access this resource.` : `An unexpected error occurred.`,
icon: 'alert'
})
}
if (networkError) {
console.error(networkError)
store.commit('showNotification', {
style: 'red',
message: `Network Error: ${networkError.message}`,
icon: 'alert'
})
}
}),
new BatchHttpLink({
includeExtensions: true,
uri: graphQLEndpoint,
credentials: 'include',
fetch: async (uri, options) => {
// Strip __typename fields from variables
let body = JSON.parse(options.body)
body = body.map(bd => {
return ({
...bd,
variables: JSON.parse(JSON.stringify(bd.variables), (key, value) => { return key === '__typename' ? undefined : value })
})
})
options.body = JSON.stringify(body)
// Inject authentication token
const jwtToken = Cookies.get('jwt')
if (jwtToken) {
options.headers.Authorization = `Bearer ${jwtToken}`
}
const resp = await fetch(uri, options)
// Handle renewed JWT
const newJWT = resp.headers.get('new-jwt')
if (newJWT) {
Cookies.set('jwt', newJWT, { expires: 365 })
}
return resp
}
})
])
window.graphQL = new ApolloClient({
link: graphQLLink,
cache: new InMemoryCache(),
connectToDevTools: (process.env.node_env === 'development')
})
// ====================================
// Initialize Vue Modules
// ====================================
Vue.config.productionTip = false
Vue.use(VueRouter)
Vue.use(VueApollo)
Vue.use(VueClipboards)
Vue.use(localization.VueI18Next)
Vue.use(helpers)
Vue.use(Vuetify)
Vue.use(VueMoment, { moment })
Vue.use(Vuescroll)
Vue.prototype.Velocity = Velocity
// ====================================
// Register Vue Components
// ====================================
Vue.component('Comments', () => import(/* webpackChunkName: "comments" */ './components/comments.vue'))
Vue.component('Editor', () => import(/* webpackPrefetch: -100, webpackChunkName: "editor" */ './components/editor.vue'))
Vue.component('History', () => import(/* webpackChunkName: "history" */ './components/history.vue'))
Vue.component('Loader', () => import(/* webpackPrefetch: true, webpackChunkName: "ui-extra" */ './components/common/loader.vue'))
Vue.component('NavHeader', () => import(/* webpackMode: "eager" */ './components/common/nav-header.vue'))
Vue.component('NewPage', () => import(/* webpackChunkName: "new-page" */ './components/new-page.vue'))
Vue.component('Notify', () => import(/* webpackMode: "eager" */ './components/common/notify.vue'))
Vue.component('NotFound', () => import(/* webpackChunkName: "not-found" */ './components/not-found.vue'))
Vue.component('PageSelector', () => import(/* webpackPrefetch: true, webpackChunkName: "ui-extra" */ './components/common/page-selector.vue'))
Vue.component('PageSource', () => import(/* webpackChunkName: "source" */ './components/source.vue'))
Vue.component('SearchResults', () => import(/* webpackPrefetch: true, webpackChunkName: "ui-extra" */ './components/common/search-results.vue'))
Vue.component('SocialSharing', () => import(/* webpackPrefetch: true, webpackChunkName: "ui-extra" */ './components/common/social-sharing.vue'))
Vue.component('Tags', () => import(/* webpackChunkName: "tags" */ './components/tags.vue'))
Vue.component('Unauthorized', () => import(/* webpackChunkName: "unauthorized" */ './components/unauthorized.vue'))
Vue.component('VCardChin', () => import(/* webpackPrefetch: true, webpackChunkName: "ui-extra" */ './components/common/v-card-chin.vue'))
Vue.component('VCardInfo', () => import(/* webpackPrefetch: true, webpackChunkName: "ui-extra" */ './components/common/v-card-info.vue'))
Vue.component('NavFooter', () => import(/* webpackChunkName: "theme" */ './themes/' + siteConfig.theme + '/components/nav-footer.vue'))
Vue.component('Page', () => import(/* webpackChunkName: "theme" */ './themes/' + siteConfig.theme + '/components/page.vue'))
let bootstrap = () => {
// ====================================
// Notifications
// ====================================
window.addEventListener('beforeunload', () => {
store.dispatch('startLoading')
})
const apolloProvider = new VueApollo({
defaultClient: window.graphQL
})
// ====================================
// Bootstrap Vue
// ====================================
const i18n = localization.init()
let darkModeEnabled = siteConfig.darkMode
if ((store.get('user/appearance') || '').length > 0) {
darkModeEnabled = (store.get('user/appearance') === 'dark')
}
window.WIKI = new Vue({
el: '#root',
components: {},
mixins: [helpers],
apolloProvider,
store,
i18n,
vuetify: new Vuetify({
rtl: siteConfig.rtl,
theme: {
dark: darkModeEnabled
}
}),
mounted () {
this.$moment.locale(siteConfig.lang)
if ((store.get('user/dateFormat') || '').length > 0) {
this.$moment.updateLocale(this.$moment.locale(), {
longDateFormat: {
'L': store.get('user/dateFormat')
}
})
}
if ((store.get('user/timezone') || '').length > 0) {
this.$moment.tz.setDefault(store.get('user/timezone'))
}
}
})
// ----------------------------------
// Dispatch boot ready
// ----------------------------------
window.boot.notify('vue')
}
window.boot.onDOMReady(bootstrap)
<template lang='pug'>
v-toolbar.radius-7(flat, :color='$vuetify.theme.dark ? "grey darken-4-l3" : "grey lighten-3"')
.body-2.mr-3 {{$t('common:duration.every')}}
v-text-field(
solo
hide-details
flat
reverse
v-model='minutes'
style='flex: 1 1 70px;'
)
.body-2.mx-3 {{$t('common:duration.minutes')}}
v-divider.mr-3
v-text-field(
solo
hide-details
flat
reverse
v-model='hours'
style='flex: 1 1 70px;'
)
.body-2.mx-3 {{$t('common:duration.hours')}}
v-divider.mr-3
v-text-field(
solo
hide-details
flat
reverse
v-model='days'
style='flex: 1 1 70px;'
)
.body-2.mx-3 {{$t('common:duration.days')}}
v-divider.mr-3
v-text-field(
solo
hide-details
flat
reverse
v-model='months'
style='flex: 1 1 70px;'
)
.body-2.mx-3 {{$t('common:duration.months')}}
v-divider.mr-3
v-text-field(
solo
hide-details
flat
reverse
v-model='years'
style='flex: 1 1 70px;'
)
.body-2.mx-3 {{$t('common:duration.years')}}
</template>
<script>
import _ from 'lodash'
import moment from 'moment'
export default {
props: {
value: {
type: String,
default: 'PT5M'
}
},
data() {
return {
duration: moment.duration(0)
}
},
computed: {
years: {
get() { return this.duration.years() || 0 },
set(val) { this.rebuild(_.toNumber(val), 'years') }
},
months: {
get() { return this.duration.months() || 0 },
set(val) { this.rebuild(_.toNumber(val), 'months') }
},
days: {
get() { return this.duration.days() || 0 },
set(val) { this.rebuild(_.toNumber(val), 'days') }
},
hours: {
get() { return this.duration.hours() || 0 },
set(val) { this.rebuild(_.toNumber(val), 'hours') }
},
minutes: {
get() { return this.duration.minutes() || 0 },
set(val) { this.rebuild(_.toNumber(val), 'minutes') }
}
},
watch: {
value(newValue, oldValue) {
this.duration = moment.duration(newValue)
}
},
methods: {
rebuild(val, unit) {
if (!_.isFinite(val) || val < 0) {
val = 0
}
const newDuration = {
minutes: this.duration.minutes(),
hours: this.duration.hours(),
days: this.duration.days(),
months: this.duration.months(),
years: this.duration.years()
}
_.set(newDuration, unit, val)
this.duration = moment.duration(newDuration)
this.$emit('input', this.duration.toISOString())
}
},
mounted() {
this.duration = moment.duration(this.value)
}
}
</script>
<template lang='pug'>
v-dialog(v-model='value', persistent, max-width='350', :overlay-color='color', overlay-opacity='.7')
v-card.loader-dialog.radius-7(:color='color', dark)
v-card-text.text-center.py-4
atom-spinner.is-inline(
v-if='mode === `loading`'
:animation-duration='1000'
:size='60'
color='#FFF'
)
img(v-else-if='mode === `icon`', :src='`/_assets-legacy/svg/icon-` + icon + `.svg`', :alt='icon')
.subtitle-1.white--text {{ title }}
.caption {{ subtitle }}
</template>
<script>
import { AtomSpinner } from 'epic-spinners'
export default {
components: {
AtomSpinner
},
props: {
value: {
type: Boolean,
default: false
},
color: {
type: String,
default: 'blue darken-3'
},
title: {
type: String,
default: 'Working...'
},
subtitle: {
type: String,
default: 'Please wait'
},
mode: {
type: String,
default: 'loading'
},
icon: {
type: String,
default: 'checkmark'
}
}
}
</script>
<style lang='scss'>
.loader-dialog {
transition: all .4s ease;
.atom-spinner.is-inline {
display: inline-block;
}
.caption {
color: rgba(255,255,255,.7);
}
img {
width: 80px;
}
}
</style>
<template lang='pug'>
v-snackbar.nav-notify(
:color='notification.style'
top
multi-line
v-model='notificationState'
:timeout='6000'
)
.text-left
v-icon.mr-3(dark) mdi-{{ notification.icon }}
span {{ notification.message }}
</template>
<script>
import { get, sync } from 'vuex-pathify'
export default {
data() {
return { }
},
computed: {
notification: get('notification'),
notificationState: sync('notification@isActive')
}
}
</script>
<style lang='scss'>
.nav-notify {
top: -64px;
padding-top: 0;
z-index: 999;
.v-snack__wrapper {
border-top-left-radius: 0;
border-top-right-radius: 0;
position: relative;
margin-top: 0;
&::after {
content: '';
display: block;
width: 100%;
height: 2px;
background-color: rgba(255,255,255,.4);
position: absolute;
bottom: 0;
left: 0;
animation: nav-notify-anim 6s linear;
}
}
}
@keyframes nav-notify-anim {
0% {
width: 100%;
}
100% {
width: 0%;
}
}
</style>
<template lang='pug'>
v-dialog(
v-model='isShown'
max-width='550'
persistent
overlay-color='blue-grey darken-4'
overlay-opacity='.7'
)
v-card
.dialog-header.is-short.is-dark
v-icon.mr-2(color='white') mdi-lightning-bolt
span {{$t('common:page.convert')}}
v-card-text.pt-5
i18next.body-2(path='common:page.convertTitle', tag='div')
span.blue-grey--text.text--darken-2(place='title') {{pageTitle}}
v-select.mt-5(
:items=`[
{ value: 'markdown', text: 'Markdown' },
{ value: 'ckeditor', text: 'Visual Editor' },
{ value: 'code', text: 'Raw HTML' }
]`
outlined
dense
hide-details
v-model='newEditor'
)
.caption.mt-5 {{$t('common:page.convertSubtitle')}}
v-card-chin
v-spacer
v-btn(text, @click='discard', :disabled='loading') {{$t('common:actions.cancel')}}
v-btn.px-4(color='grey darken-3', @click='convertPage', :loading='loading').white--text {{$t('common:actions.convert')}}
</template>
<script>
import _ from 'lodash'
import { get } from 'vuex-pathify'
import gql from 'graphql-tag'
export default {
props: {
value: {
type: Boolean,
default: false
}
},
data() {
return {
loading: false,
newEditor: ''
}
},
computed: {
isShown: {
get() { return this.value },
set(val) { this.$emit('input', val) }
},
pageTitle: get('page/title'),
pagePath: get('page/path'),
pageLocale: get('page/locale'),
pageId: get('page/id'),
pageEditor: get('page/editor')
},
mounted () {
this.newEditor = this.pageEditor
},
methods: {
discard() {
this.isShown = false
},
async convertPage() {
this.loading = true
this.$store.commit(`loadingStart`, 'page-convert')
this.$nextTick(async () => {
try {
const resp = await this.$apollo.mutate({
mutation: gql`
mutation (
$id: Int!
$editor: String!
) {
pages {
convert(
id: $id
editor: $editor
) {
responseResult {
succeeded
errorCode
slug
message
}
}
}
}
`,
variables: {
id: this.pageId,
editor: this.newEditor
}
})
if (_.get(resp, 'data.pages.convert.responseResult.succeeded', false)) {
this.isShown = false
window.location.assign(`/e/${this.pageLocale}/${this.pagePath}`)
} else {
throw new Error(_.get(resp, 'data.pages.convert.responseResult.message', this.$t('common:error.unexpected')))
}
} catch (err) {
this.$store.commit('pushGraphError', err)
}
this.$store.commit(`loadingStop`, 'page-convert')
this.loading = false
})
}
}
}
</script>
<style lang='scss'>
</style>
<template lang='pug'>
v-dialog(
v-model='isShown'
max-width='550'
persistent
overlay-color='red darken-4'
overlay-opacity='.7'
)
v-card
.dialog-header.is-short.is-red
v-icon.mr-2(color='white') mdi-file-document-box-remove-outline
span {{$t('common:page.delete')}}
v-card-text.pt-5
i18next.body-1(path='common:page.deleteTitle', tag='div')
span.red--text.text--darken-2(place='title') {{pageTitle}}
.caption {{$t('common:page.deleteSubtitle')}}
v-chip.mt-3.ml-0.mr-1(label, color='red lighten-4', small)
.caption.red--text.text--darken-2 {{pageLocale.toUpperCase()}}
v-chip.mt-3.mx-0(label, color='red lighten-5', small)
span.red--text.text--darken-2 /{{pagePath}}
v-card-chin
v-spacer
v-btn(text, @click='discard', :disabled='loading') {{$t('common:actions.cancel')}}
v-btn.px-4(color='red darken-2', @click='deletePage', :loading='loading').white--text {{$t('common:actions.delete')}}
</template>
<script>
import _ from 'lodash'
import { get } from 'vuex-pathify'
import deletePageMutation from 'gql/common/common-pages-mutation-delete.gql'
export default {
props: {
value: {
type: Boolean,
default: false
}
},
data() {
return {
loading: false
}
},
computed: {
isShown: {
get() { return this.value },
set(val) { this.$emit('input', val) }
},
pageTitle: get('page/title'),
pagePath: get('page/path'),
pageLocale: get('page/locale'),
pageId: get('page/id')
},
watch: {
isShown(newValue, oldValue) {
if (newValue) {
document.body.classList.add('page-deleted-pending')
}
}
},
methods: {
discard() {
document.body.classList.remove('page-deleted-pending')
this.isShown = false
},
async deletePage() {
this.loading = true
this.$store.commit(`loadingStart`, 'page-delete')
this.$nextTick(async () => {
try {
const resp = await this.$apollo.mutate({
mutation: deletePageMutation,
variables: {
id: this.pageId
}
})
if (_.get(resp, 'data.pages.delete.responseResult.succeeded', false)) {
this.isShown = false
_.delay(() => {
document.body.classList.add('page-deleted')
_.delay(() => {
window.location.assign('/')
}, 1200)
}, 400)
} else {
throw new Error(_.get(resp, 'data.pages.delete.responseResult.message', this.$t('common:error.unexpected')))
}
} catch (err) {
this.$store.commit('pushGraphError', err)
}
this.$store.commit(`loadingStop`, 'page-delete')
this.loading = false
})
}
}
}
</script>
<style lang='scss'>
body.page-deleted-pending {
perspective: 50vw;
height: 100vh;
overflow: hidden;
.application {
background-color: mc('grey', '900');
}
.application--wrap {
transform-style: preserve-3d;
transform: translateZ(-5vw) rotateX(2deg);
border-radius: 7px;
overflow: hidden;
}
}
body.page-deleted {
perspective: 50vw;
.application--wrap {
transform-style: preserve-3d;
transform: translateZ(-1000vw) rotateX(60deg);
opacity: 0;
}
}
</style>
<template lang="pug">
v-dialog(
v-model='isShown'
max-width='850px'
overlay-color='blue darken-4'
overlay-opacity='.7'
)
v-card.page-selector
.dialog-header.is-blue
v-icon.mr-3(color='white') mdi-page-next-outline
.body-1(v-if='mode === `create`') {{$t('common:pageSelector.createTitle')}}
.body-1(v-else-if='mode === `move`') {{$t('common:pageSelector.moveTitle')}}
.body-1(v-else-if='mode === `select`') {{$t('common:pageSelector.selectTitle')}}
v-spacer
v-progress-circular(
indeterminate
color='white'
:size='20'
:width='2'
v-show='searchLoading'
)
.d-flex
v-flex.grey(xs5, :class='$vuetify.theme.dark ? `darken-4` : `lighten-3`')
v-toolbar(color='grey darken-3', dark, dense, flat)
.body-2 {{$t('common:pageSelector.virtualFolders')}}
v-spacer
v-btn(icon, tile, href='https://docs.requarks.io/guide/pages#folders', target='_blank')
v-icon mdi-help-box
div(style='height:400px;')
vue-scroll(:ops='scrollStyle')
v-treeview(
:key='`pageTree-` + treeViewCacheId'
:active.sync='currentNode'
:open.sync='openNodes'
:items='tree'
:load-children='fetchFolders'
dense
expand-icon='mdi-menu-down-outline'
item-id='path'
item-text='title'
activatable
hoverable
)
template(slot='prepend', slot-scope='{ item, open, leaf }')
v-icon mdi-{{ open ? 'folder-open' : 'folder' }}
v-flex(xs7)
v-toolbar(color='blue darken-2', dark, dense, flat)
.body-2 {{$t('common:pageSelector.pages')}}
//- v-spacer
//- v-btn(icon, tile, disabled): v-icon mdi-content-save-move-outline
//- v-btn(icon, tile, disabled): v-icon mdi-trash-can-outline
div(v-if='currentPages.length > 0', style='height:400px;')
vue-scroll(:ops='scrollStyle')
v-list.py-0(dense)
v-list-item-group(
v-model='currentPage'
color='primary'
)
template(v-for='(page, idx) of currentPages')
v-list-item(:key='`page-` + page.id', :value='page')
v-list-item-icon: v-icon mdi-text-box
v-list-item-title {{page.title}}
v-divider(v-if='idx < pages.length - 1')
v-alert.animated.fadeIn(
v-else
text
color='orange'
prominent
icon='mdi-alert'
)
.body-2 {{$t('common:pageSelector.folderEmptyWarning')}}
v-card-actions.grey.pa-2(:class='$vuetify.theme.dark ? `darken-2` : `lighten-1`', v-if='!mustExist')
v-select(
solo
dark
flat
background-color='grey darken-3-d2'
hide-details
single-line
:items='namespaces'
style='flex: 0 0 100px; border-radius: 4px 0 0 4px;'
v-model='currentLocale'
)
v-text-field(
ref='pathIpt'
solo
hide-details
prefix='/'
v-model='currentPath'
flat
clearable
style='border-radius: 0 4px 4px 0;'
)
v-card-chin
v-spacer
v-btn(text, @click='close') {{$t('common:actions.cancel')}}
v-btn.px-4(color='primary', @click='open', :disabled='!isValidPath')
v-icon(left) mdi-check
span {{$t('common:actions.select')}}
</template>
<script>
import _ from 'lodash'
import gql from 'graphql-tag'
const localeSegmentRegex = /^[A-Z]{2}(-[A-Z]{2})?$/i
/* global siteLangs, siteConfig */
export default {
props: {
value: {
type: Boolean,
default: false
},
path: {
type: String,
default: 'new-page'
},
locale: {
type: String,
default: 'en'
},
mode: {
type: String,
default: 'create'
},
openHandler: {
type: Function,
default: () => {}
},
mustExist: {
type: Boolean,
default: false
}
},
data() {
return {
treeViewCacheId: 0,
searchLoading: false,
currentLocale: siteConfig.lang,
currentFolderPath: '',
currentPath: 'new-page',
currentPage: null,
currentNode: [0],
openNodes: [0],
tree: [
{
id: 0,
title: '/ (root)',
children: []
}
],
pages: [],
all: [],
namespaces: siteLangs.length ? siteLangs.map(ns => ns.code) : [siteConfig.lang],
scrollStyle: {
vuescroll: {},
scrollPanel: {
initialScrollX: 0.01, // fix scrollbar not disappearing on load
scrollingX: false,
speed: 50
},
rail: {
gutterOfEnds: '2px'
},
bar: {
onlyShowBarOnScroll: false,
background: '#999',
hoverStyle: {
background: '#64B5F6'
}
}
}
}
},
computed: {
isShown: {
get() { return this.value },
set(val) { this.$emit('input', val) }
},
currentPages () {
return _.sortBy(_.filter(this.pages, ['parent', _.head(this.currentNode) || 0]), ['title', 'path'])
},
isValidPath () {
if (!this.currentPath) {
return false
}
if (this.mustExist && !this.currentPage) {
return false
}
const firstSection = _.head(this.currentPath.split('/'))
if (firstSection.length <= 1) {
return false
} else if (localeSegmentRegex.test(firstSection)) {
return false
} else if (
_.some(['login', 'logout', 'register', 'verify', 'favicons', 'fonts', 'img', 'js', 'svg'], p => {
return p === firstSection
})) {
return false
} else {
return true
}
}
},
watch: {
isShown (newValue, oldValue) {
if (newValue && !oldValue) {
this.currentPath = this.path
this.currentLocale = this.locale
_.delay(() => {
this.$refs.pathIpt.focus()
})
}
},
currentNode (newValue, oldValue) {
if (newValue.length < 1) { // force a selection
this.$nextTick(() => {
this.currentNode = oldValue
})
} else {
const current = _.find(this.all, ['id', newValue[0]])
if (this.openNodes.indexOf(newValue[0]) < 0) { // auto open and load children
if (current) {
if (this.openNodes.indexOf(current.parent) < 0) {
this.$nextTick(() => {
this.openNodes.push(current.parent)
})
}
}
this.$nextTick(() => {
this.openNodes.push(newValue[0])
})
}
this.currentPath = _.compact([_.get(current, 'path', ''), _.last(this.currentPath.split('/'))]).join('/')
}
},
currentPage (newValue, oldValue) {
if (!_.isEmpty(newValue)) {
this.currentPath = newValue.path
}
},
currentLocale (newValue, oldValue) {
this.$nextTick(() => {
this.tree = [
{
id: 0,
title: '/ (root)',
children: []
}
]
this.currentNode = [0]
this.openNodes = [0]
this.pages = []
this.all = []
this.treeViewCacheId += 1
})
}
},
methods: {
close() {
this.isShown = false
},
open() {
const exit = this.openHandler({
locale: this.currentLocale,
path: this.currentPath,
id: (this.mustExist && this.currentPage) ? this.currentPage.pageId : 0
})
if (exit !== false) {
this.close()
}
},
async fetchFolders (item) {
this.searchLoading = true
const resp = await this.$apollo.query({
query: gql`
query ($parent: Int!, $mode: PageTreeMode!, $locale: String!) {
pages {
tree(parent: $parent, mode: $mode, locale: $locale) {
id
path
title
isFolder
pageId
parent
}
}
}
`,
fetchPolicy: 'network-only',
variables: {
parent: item.id,
mode: 'ALL',
locale: this.currentLocale
}
})
const items = _.get(resp, 'data.pages.tree', [])
const itemFolders = _.filter(items, ['isFolder', true]).map(f => ({...f, children: []}))
const itemPages = _.filter(items, i => i.pageId > 0)
if (itemFolders.length > 0) {
item.children = itemFolders
} else {
item.children = undefined
}
this.pages = _.unionBy(this.pages, itemPages, 'id')
this.all = _.unionBy(this.all, items, 'id')
this.searchLoading = false
}
}
}
</script>
<style lang='scss'>
.page-selector {
.v-treeview-node__label {
font-size: 13px;
}
.v-treeview-node__content {
cursor: pointer;
}
}
</style>
<template lang="pug">
.password-strength
v-progress-linear(
:color='passwordStrengthColor'
v-model='passwordStrength'
height='2'
)
.caption(v-if='!hideText', :class='passwordStrengthColor + "--text"') {{passwordStrengthText}}
</template>
<script>
import zxcvbn from 'zxcvbn'
import _ from 'lodash'
export default {
props: {
value: {
type: String,
default: ''
},
hideText: {
type: Boolean,
default: false
}
},
data() {
return {
passwordStrength: 0,
passwordStrengthColor: 'grey',
passwordStrengthText: ''
}
},
watch: {
value(newValue) {
this.checkPasswordStrength(newValue)
}
},
methods: {
checkPasswordStrength: _.debounce(function (pwd) {
if (!pwd || pwd.length < 1) {
this.passwordStrength = 0
this.passwordStrengthColor = 'grey'
this.passwordStrengthText = ''
return
}
const strength = zxcvbn(pwd)
this.passwordStrength = _.round((strength.score + 1) / 5 * 100)
if (this.passwordStrength <= 20) {
this.passwordStrengthColor = 'red'
this.passwordStrengthText = this.$t('common:password.veryWeak')
} else if (this.passwordStrength <= 40) {
this.passwordStrengthColor = 'orange'
this.passwordStrengthText = this.$t('common:password.weak')
} else if (this.passwordStrength <= 60) {
this.passwordStrengthColor = 'teal'
this.passwordStrengthText = this.$t('common:password.average')
} else if (this.passwordStrength <= 80) {
this.passwordStrengthColor = 'green'
this.passwordStrengthText = this.$t('common:password.strong')
} else {
this.passwordStrengthColor = 'green'
this.passwordStrengthText = this.$t('common:password.veryStrong')
}
}, 100)
}
}
</script>
<style lang="scss">
.password-strength > .caption {
width: 100%;
left: 0;
margin: 0;
position: absolute;
top: calc(100% + 5px);
}
</style>
<template lang="pug">
.search-results(v-if='searchIsFocused || (search && search.length > 1)')
.search-results-container
.search-results-help(v-if='!search || (search && search.length < 2)')
img(src='/_assets-legacy/svg/icon-search-alt.svg')
.mt-4 {{$t('common:header.searchHint')}}
.search-results-loader(v-else-if='searchIsLoading && (!results || results.length < 1)')
orbit-spinner(
:animation-duration='1000'
:size='100'
color='#FFF'
)
.headline.mt-5 {{$t('common:header.searchLoading')}}
.search-results-none(v-else-if='!searchIsLoading && (!results || results.length < 1)')
img(src='/_assets-legacy/svg/icon-no-results.svg', alt='No Results')
.subheading {{$t('common:header.searchNoResult')}}
template(v-if='search && search.length >= 2 && results && results.length > 0')
v-subheader.white--text {{$t('common:header.searchResultsCount', { total: response.totalHits })}}
v-list.search-results-items.radius-7.py-0(two-line, dense)
template(v-for='(item, idx) of results')
v-list-item(@click='goToPage(item)', @click.middle="goToPageInNewTab(item)", :key='item.id', :class='idx === cursor ? `highlighted` : ``')
v-list-item-avatar(tile)
img(src='/_assets-legacy/svg/icon-selective-highlighting.svg')
v-list-item-content
v-list-item-title(v-text='item.title')
v-list-item-subtitle.caption(v-text='item.description')
.caption.grey--text(v-text='item.path')
v-list-item-action
v-chip(label, outlined) {{item.locale.toUpperCase()}}
v-divider(v-if='idx < results.length - 1')
v-pagination.mt-3(
v-if='paginationLength > 1'
dark
v-model='pagination'
:length='paginationLength'
circle
)
template(v-if='suggestions && suggestions.length > 0')
v-subheader.white--text.mt-3 {{$t('common:header.searchDidYouMean')}}
v-list.search-results-suggestions.radius-7(dense, dark)
template(v-for='(term, idx) of suggestions')
v-list-item(:key='term', @click='setSearchTerm(term)', :class='idx + results.length === cursor ? `highlighted` : ``')
v-list-item-avatar
v-icon mdi-magnify
v-list-item-content
v-list-item-title(v-text='term')
v-divider(v-if='idx < suggestions.length - 1')
.text-xs-center.pt-5(v-if='search && search.length > 1')
//- v-btn.mx-2(outlined, color='orange', @click='search = ``', v-if='results.length > 0')
//- v-icon(left) mdi-content-save
//- span {{$t('common:header.searchCopyLink')}}
v-btn.mx-2(outlined, color='pink', @click='search = ``')
v-icon(left) mdi-close
span {{$t('common:header.searchClose')}}
</template>
<script>
import _ from 'lodash'
import { sync } from 'vuex-pathify'
import { OrbitSpinner } from 'epic-spinners'
import searchPagesQuery from 'gql/common/common-pages-query-search.gql'
export default {
components: {
OrbitSpinner
},
data() {
return {
cursor: 0,
pagination: 1,
perPage: 10,
response: {
results: [],
suggestions: [],
totalHits: 0
}
}
},
computed: {
search: sync('site/search'),
searchIsFocused: sync('site/searchIsFocused'),
searchIsLoading: sync('site/searchIsLoading'),
searchRestrictLocale: sync('site/searchRestrictLocale'),
searchRestrictPath: sync('site/searchRestrictPath'),
results() {
const currentIndex = (this.pagination - 1) * this.perPage
return this.response.results ? _.slice(this.response.results, currentIndex, currentIndex + this.perPage) : []
},
hits() {
return this.response.totalHits ? this.response.totalHits : 0
},
suggestions() {
return this.response.suggestions ? this.response.suggestions : []
},
paginationLength() {
return (this.response.totalHits > 0) ? Math.ceil(this.response.totalHits / this.perPage) : 0
}
},
watch: {
search(newValue, oldValue) {
this.cursor = 0
if (!newValue || (newValue && newValue.length < 2)) {
this.searchIsLoading = false
} else {
this.searchIsLoading = true
}
}
},
mounted() {
this.$root.$on('searchMove', (dir) => {
this.cursor += ((dir === 'up') ? -1 : 1)
if (this.cursor < -1) {
this.cursor = -1
} else if (this.cursor > this.results.length + this.suggestions.length - 1) {
this.cursor = this.results.length + this.suggestions.length - 1
}
})
this.$root.$on('searchEnter', () => {
if (!this.results) {
return
}
if (this.cursor >= 0 && this.cursor < this.results.length) {
this.goToPage(_.nth(this.results, this.cursor))
} else if (this.cursor >= 0) {
this.setSearchTerm(_.nth(this.suggestions, this.cursor - this.results.length))
}
})
},
methods: {
setSearchTerm(term) {
this.search = term
},
goToPage(item) {
window.location.assign(`/${item.locale}/${item.path}`)
},
goToPageInNewTab(item) {
window.open(`/${item.locale}/${item.path}`, '_blank')
}
},
apollo: {
response: {
query: searchPagesQuery,
variables() {
return {
query: this.search
}
},
fetchPolicy: 'network-only',
debounce: 300,
throttle: 1000,
skip() {
return !this.search || this.search.length < 2
},
update: (data) => _.get(data, 'pages.search', {}),
watchLoading (isLoading) {
this.searchIsLoading = isLoading
}
}
}
}
</script>
<style lang="scss">
.search-results {
position: fixed;
top: 64px;
left: 0;
overflow-y: auto;
width: 100%;
height: calc(100% - 64px);
background-color: rgba(0,0,0,.9);
z-index: 100;
text-align: center;
animation: searchResultsReveal .6s ease;
@media #{map-get($display-breakpoints, 'sm-and-down')} {
top: 112px;
}
&-container {
margin: 12px auto;
width: 90vw;
max-width: 1024px;
}
&-help {
text-align: center;
padding: 32px 0;
font-size: 18px;
font-weight: 300;
color: #FFF;
img {
width: 104px;
}
}
&-loader {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
padding: 32px 0;
color: #FFF;
}
&-none {
color: #FFF;
img {
width: 200px;
}
}
&-items {
text-align: left;
.highlighted {
background: #FFF linear-gradient(to bottom, #FFF, mc('orange', '100'));
@at-root .theme--dark & {
background: mc('grey', '900') linear-gradient(to bottom, mc('orange', '900'), darken(mc('orange', '900'), 15%));
}
}
}
&-suggestions {
.highlighted {
background: transparent linear-gradient(to bottom, mc('blue', '500'), mc('blue', '700'));
}
}
}
@keyframes searchResultsReveal {
0% {
background-color: rgba(0,0,0,0);
padding-top: 32px;
}
100% {
background-color: rgba(0,0,0,.9);
padding-top: 0;
}
}
</style>
<template lang="pug">
v-list(nav, dense)
v-list-item(@click='', ref='copyUrlButton')
v-icon(color='grey', small) mdi-content-copy
v-list-item-title.px-3 Copy URL
v-list-item(:href='`mailto:?subject=` + encodeURIComponent(title) + `&body=` + encodeURIComponent(url) + `%0D%0A%0D%0A` + encodeURIComponent(description)')
v-icon(color='grey', small) mdi-email-outline
v-list-item-title.px-3 Email
v-list-item(@click='openSocialPop(`https://www.facebook.com/sharer/sharer.php?u=` + encodeURIComponent(url) + `&title=` + encodeURIComponent(title) + `&description=` + encodeURIComponent(description))')
v-icon(color='grey', small) mdi-facebook
v-list-item-title.px-3 Facebook
v-list-item(@click='openSocialPop(`https://www.linkedin.com/shareArticle?mini=true&url=` + encodeURIComponent(url) + `&title=` + encodeURIComponent(title) + `&summary=` + encodeURIComponent(description))')
v-icon(color='grey', small) mdi-linkedin
v-list-item-title.px-3 LinkedIn
v-list-item(@click='openSocialPop(`https://www.reddit.com/submit?url=` + encodeURIComponent(url) + `&title=` + encodeURIComponent(title))')
v-icon(color='grey', small) mdi-reddit
v-list-item-title.px-3 Reddit
v-list-item(@click='openSocialPop(`https://t.me/share/url?url=` + encodeURIComponent(url) + `&text=` + encodeURIComponent(title))')
v-icon(color='grey', small) mdi-telegram
v-list-item-title.px-3 Telegram
v-list-item(@click='openSocialPop(`https://twitter.com/intent/tweet?url=` + encodeURIComponent(url) + `&text=` + encodeURIComponent(title))')
v-icon(color='grey', small) mdi-twitter
v-list-item-title.px-3 Twitter
v-list-item(:href='`viber://forward?text=` + encodeURIComponent(url) + ` ` + encodeURIComponent(description)')
v-icon(color='grey', small) mdi-phone-in-talk
v-list-item-title.px-3 Viber
v-list-item(@click='openSocialPop(`http://service.weibo.com/share/share.php?url=` + encodeURIComponent(url) + `&title=` + encodeURIComponent(title))')
v-icon(color='grey', small) mdi-sina-weibo
v-list-item-title.px-3 Weibo
v-list-item(@click='openSocialPop(`https://api.whatsapp.com/send?text=` + encodeURIComponent(title) + `%0D%0A` + encodeURIComponent(url))')
v-icon(color='grey', small) mdi-whatsapp
v-list-item-title.px-3 Whatsapp
</template>
<script>
import ClipboardJS from 'clipboard'
export default {
props: {
url: {
type: String,
default: window.location.url
},
title: {
type: String,
default: 'Untitled Page'
},
description: {
type: String,
default: ''
}
},
data () {
return {
width: 626,
height: 436,
left: 0,
top: 0
}
},
methods: {
openSocialPop (url) {
const popupWindow = window.open(
url,
'sharer',
`status=no,height=${this.height},width=${this.width},resizable=yes,left=${this.left},top=${this.top},screenX=${this.left},screenY=${this.top},toolbar=no,menubar=no,scrollbars=no,location=no,directories=no`
)
popupWindow.focus()
}
},
mounted () {
const clip = new ClipboardJS(this.$refs.copyUrlButton.$el, {
text: () => { return this.url }
})
clip.on('success', () => {
this.$store.commit('showNotification', {
style: 'success',
message: `URL copied successfully`,
icon: 'content-copy'
})
})
clip.on('error', () => {
this.$store.commit('showNotification', {
style: 'red',
message: `Failed to copy to clipboard`,
icon: 'alert'
})
})
/**
* Center the popup on dual screens
* http://stackoverflow.com/questions/4068373/center-a-popup-window-on-screen/32261263
*/
const dualScreenLeft = window.screenLeft !== undefined ? window.screenLeft : screen.left
const dualScreenTop = window.screenTop !== undefined ? window.screenTop : screen.top
const width = window.innerWidth ? window.innerWidth : (document.documentElement.clientWidth ? document.documentElement.clientWidth : screen.width)
const height = window.innerHeight ? window.innerHeight : (document.documentElement.clientHeight ? document.documentElement.clientHeight : screen.height)
this.left = ((width / 2) - (this.width / 2)) + dualScreenLeft
this.top = ((height / 2) - (this.height / 2)) + dualScreenTop
}
}
</script>
<template lang="pug">
v-dialog(
v-model='dialogOpen'
max-width='650'
)
v-card
.dialog-header
span {{$t('common:user.search')}}
v-spacer
v-progress-circular(
indeterminate
color='white'
:size='20'
:width='2'
v-show='searchLoading'
)
v-card-text.pt-5
v-text-field(
outlined
:label='$t(`common:user.searchPlaceholder`)'
v-model='search'
prepend-inner-icon='mdi-account-search-outline'
color='primary'
ref='searchIpt'
hide-details
)
v-list.grey.mt-3.py-0.radius-7(
:class='$vuetify.theme.dark ? `darken-3-d5` : `lighten-3`'
two-line
dense
)
template(v-for='(usr, idx) in items')
v-list-item(:key='usr.id', @click='setUser(usr)')
v-list-item-avatar(size='40', color='primary')
span.body-1.white--text {{usr.name | initials}}
v-list-item-content
v-list-item-title.body-2 {{usr.name}}
v-list-item-subtitle {{usr.email}}
v-list-item-action
v-icon(color='primary') mdi-arrow-right
v-divider.my-0(v-if='idx < items.length - 1')
v-card-chin
v-spacer
v-btn(
text
@click='close'
:disabled='loading'
) {{$t('common:actions.cancel')}}
</template>
<script>
import _ from 'lodash'
import gql from 'graphql-tag'
export default {
filters: {
initials(val) {
return val.split(' ').map(v => v.substring(0, 1)).join('')
}
},
props: {
multiple: {
type: Boolean,
default: false
},
value: {
type: Boolean,
default: false
}
},
data() {
return {
loading: false,
searchLoading: false,
search: '',
items: []
}
},
computed: {
dialogOpen: {
get() { return this.value },
set(value) { this.$emit('input', value) }
}
},
watch: {
value(newValue, oldValue) {
if (newValue && !oldValue) {
this.search = ''
this.selectedItems = null
_.delay(() => { this.$refs.searchIpt.focus() }, 100)
}
}
},
methods: {
close() {
this.$emit('input', false)
},
setUser(usr) {
this.$emit('select', usr)
this.close()
},
searchFilter(item, queryText, itemText) {
return _.includes(_.toLower(item.email), _.toLower(queryText)) || _.includes(_.toLower(item.name), _.toLower(queryText))
}
},
apollo: {
items: {
query: gql`
query ($query: String!) {
users {
search(query:$query) {
id
name
email
providerKey
}
}
}
`,
variables() {
return {
query: this.search
}
},
fetchPolicy: 'cache-and-network',
skip() {
return !this.search || this.search.length < 2
},
update: (data) => data.users.search,
watchLoading (isLoading) {
this.searchLoading = isLoading
}
}
}
}
</script>
<template lang='pug'>
div
v-divider.my-0
v-card-actions(:class='$vuetify.theme.dark ? "grey darken-4-l5" : "grey lighten-4"')
slot
</template>
<script>
export default { }
</script>
<template lang='pug'>
.v-card-info(:class='`is-` + color')
v-card-text.d-flex.align-center(:class='colors.cls')
v-icon(:color='colors.icon', left) {{icon}}
slot
</template>
<script>
export default {
props: {
color: {
type: String,
default: 'blue'
},
icon: {
type: String,
default: 'mdi-information-outline'
}
},
computed: {
colors () {
switch (this.color) {
case 'blue':
return {
cls: this.$vuetify.theme.dark ? 'grey darken-4-l5 blue--text text--lighten-4' : 'blue lighten-5 blue--text text--darken-3',
icon: 'blue lighten-3'
}
case 'red':
return {
cls: this.$vuetify.theme.dark ? 'grey darken-4-l5 red--text text--lighten-4' : 'red lighten-5 red--text text--darken-2',
icon: 'red lighten-3'
}
default:
return {
cls: this.$vuetify.theme.dark ? 'grey darken-4-l5' : 'grey lighten-4',
icon: 'grey darken-2'
}
}
}
}
}
</script>
<style lang="scss">
.v-card-info {
border-bottom: 1px solid #EEE;
&.is-blue {
border-bottom-color: mc('blue', '100');
@at-root .theme--dark & {
border-bottom-color: rgba(mc('blue', '100'), .3);
}
}
&.is-red {
border-bottom-color: mc('red', '100');
@at-root .theme--dark & {
border-bottom-color: rgba(mc('red', '100'), .3);
}
}
}
</style>
<template lang="pug">
</template>
<script>
export default {
}
</script>
<template lang="pug">
v-dialog(
v-model='isShown'
max-width='700'
)
v-card
.dialog-header.is-short.is-indigo
v-icon.mr-2(color='white') mdi-alert
span {{$t('editor:conflict.title')}}
v-card-text.pt-4
i18next.body-2(tag='div', path='editor:conflict.infoGeneric')
strong(place='authorName') {{latest.authorName}}
span(place='date', :title='$options.filters.moment(latest.updatedAt, `LLL`)') {{ latest.updatedAt | moment('from') }}.
v-btn.mt-2(outlined, color='indigo', small, :href='`/` + latest.locale + `/` + latest.path', target='_blank')
v-icon(left) mdi-open-in-new
span {{$t('editor:conflict.viewLatestVersion')}}
.body-2.mt-5: strong {{$t('editor:conflict.whatToDo')}}
.body-2.mt-1 #[v-icon(color='indigo') mdi-alpha-l-box] {{$t('editor:conflict.whatToDoLocal')}}
.body-2.mt-1 #[v-icon(color='indigo') mdi-alpha-r-box] {{$t('editor:conflict.whatToDoRemote')}}
v-card-chin
v-spacer
v-btn(text, @click='close') {{$t('common:actions.cancel')}}
v-btn.px-4(color='indigo', @click='useLocal', dark, :title='$t(`editor:conflict.useLocalHint`)')
v-icon(left) mdi-alpha-l-box
span {{$t('editor:conflict.useLocal')}}
v-dialog(
v-model='isRemoteConfirmDiagShown'
width='500'
)
template(v-slot:activator='{ on }')
v-btn.ml-3(color='indigo', dark, v-on='on', :title='$t(`editor:conflict.useRemoteHint`)')
v-icon(left) mdi-alpha-r-box
span {{$t('editor:conflict.useRemote')}}
v-card
.dialog-header.is-short.is-indigo
v-icon.mr-3(color='white') mdi-alpha-r-box
span {{$t('editor:conflict.overwrite.title')}}
v-card-text.pa-4
i18next.body-2(tag='div', path='editor:conflict.overwrite.description')
strong(place='refEditsLost') {{$t('editor:conflict.overwrite.editsLost')}}
v-card-chin
v-spacer
v-btn(outlined, color='indigo', @click='isRemoteConfirmDiagShown = false')
v-icon(left) mdi-close
span {{$t('common:actions.cancel')}}
v-btn(@click='useRemote', color='indigo', dark)
v-icon(left) mdi-check
span {{$t('common:actions.confirm')}}
</template>
<script>
import _ from 'lodash'
import gql from 'graphql-tag'
export default {
props: {
value: {
type: Boolean,
default: false
}
},
data() {
return {
latest: {
updatedAt: '',
authorName: '',
content: '',
locale: '',
path: ''
},
isRemoteConfirmDiagShown: false
}
},
computed: {
isShown: {
get() { return this.value },
set(val) { this.$emit('input', val) }
}
},
methods: {
close () {
this.isShown = false
},
useLocal () {
this.$store.set('editor/checkoutDateActive', this.latest.updatedAt)
this.$root.$emit('resetEditorConflict')
this.close()
},
useRemote () {
this.$store.set('editor/checkoutDateActive', this.latest.updatedAt)
this.$store.set('editor/content', this.latest.content)
this.$root.$emit('overwriteEditorContent')
this.$root.$emit('resetEditorConflict')
this.close()
}
},
async mounted () {
let resp = await this.$apollo.query({
query: gql`
query ($id: Int!) {
pages {
conflictLatest(id: $id) {
authorName
locale
path
content
updatedAt
}
}
}
`,
fetchPolicy: 'network-only',
variables: {
id: this.$store.get('page/id')
}
})
resp = _.get(resp, 'data.pages.conflictLatest', false)
if (!resp) {
return this.$store.commit('showNotification', {
message: 'Failed to fetch latest version.',
style: 'warning',
icon: 'warning'
})
}
this.latest = resp
}
}
</script>
// Test if potential opening or closing delimieter
// Assumes that there is a "$" at state.src[pos]
function isValidDelim (state, pos) {
let prevChar
let nextChar
let max = state.posMax
let canOpen = true
let canClose = true
prevChar = pos > 0 ? state.src.charCodeAt(pos - 1) : -1
nextChar = pos + 1 <= max ? state.src.charCodeAt(pos + 1) : -1
// Check non-whitespace conditions for opening and closing, and
// check that closing delimeter isn't followed by a number
if (prevChar === 0x20/* " " */ || prevChar === 0x09/* \t */ ||
(nextChar >= 0x30/* "0" */ && nextChar <= 0x39/* "9" */)) {
canClose = false
}
if (nextChar === 0x20/* " " */ || nextChar === 0x09/* \t */) {
canOpen = false
}
return {
canOpen: canOpen,
canClose: canClose
}
}
export default {
katexInline (state, silent) {
let start, match, token, res, pos
if (state.src[state.pos] !== '$') { return false }
res = isValidDelim(state, state.pos)
if (!res.canOpen) {
if (!silent) { state.pending += '$' }
state.pos += 1
return true
}
// First check for and bypass all properly escaped delimieters
// This loop will assume that the first leading backtick can not
// be the first character in state.src, which is known since
// we have found an opening delimieter already.
start = state.pos + 1
match = start
while ((match = state.src.indexOf('$', match)) !== -1) {
// Found potential $, look for escapes, pos will point to
// first non escape when complete
pos = match - 1
while (state.src[pos] === '\\') { pos -= 1 }
// Even number of escapes, potential closing delimiter found
if (((match - pos) % 2) === 1) { break }
match += 1
}
// No closing delimter found. Consume $ and continue.
if (match === -1) {
if (!silent) { state.pending += '$' }
state.pos = start
return true
}
// Check if we have empty content, ie: $$. Do not parse.
if (match - start === 0) {
if (!silent) { state.pending += '$$' }
state.pos = start + 1
return true
}
// Check for valid closing delimiter
res = isValidDelim(state, match)
if (!res.canClose) {
if (!silent) { state.pending += '$' }
state.pos = start
return true
}
if (!silent) {
token = state.push('katex_inline', 'math', 0)
token.markup = '$'
token.content = state.src
// Extract the math part without the $
.slice(start, match)
// Escape the curly braces since they will be interpreted as
// attributes by markdown-it-attrs (the "curly_attributes"
// core rule)
.replaceAll("{", "{{")
.replaceAll("}", "}}")
}
state.pos = match + 1
return true
},
katexBlock (state, start, end, silent) {
let firstLine; let lastLine; let next; let lastPos; let found = false; let token
let pos = state.bMarks[start] + state.tShift[start]
let max = state.eMarks[start]
if (pos + 2 > max) { return false }
if (state.src.slice(pos, pos + 2) !== '$$') { return false }
pos += 2
firstLine = state.src.slice(pos, max)
if (silent) { return true }
if (firstLine.trim().slice(-2) === '$$') {
// Single line expression
firstLine = firstLine.trim().slice(0, -2)
found = true
}
for (next = start; !found;) {
next++
if (next >= end) { break }
pos = state.bMarks[next] + state.tShift[next]
max = state.eMarks[next]
if (pos < max && state.tShift[next] < state.blkIndent) {
// non-empty line with negative indent should stop the list:
break
}
if (state.src.slice(pos, max).trim().slice(-2) === '$$') {
lastPos = state.src.slice(0, max).lastIndexOf('$$')
lastLine = state.src.slice(pos, lastPos)
found = true
}
}
state.line = next + 1
token = state.push('katex_block', 'math', 0)
token.block = true
token.content = (firstLine && firstLine.trim() ? firstLine + '\n' : '') +
state.getLines(start + 1, next, state.tShift[start], true) +
(lastLine && lastLine.trim() ? lastLine : '')
token.map = [ start, state.line ]
token.markup = '$$'
return true
}
}
<template lang='pug'>
.editor-ckeditor
div(ref='toolbarContainer')
div.contents(ref='editor')
v-system-bar.editor-ckeditor-sysbar(dark, status, color='grey darken-3')
.caption.editor-ckeditor-sysbar-locale {{locale.toUpperCase()}}
.caption.px-3 /{{path}}
template(v-if='$vuetify.breakpoint.mdAndUp')
v-spacer
.caption Visual Editor
v-spacer
.caption {{$t('editor:ckeditor.stats', { chars: stats.characters, words: stats.words })}}
editor-conflict(v-model='isConflict', v-if='isConflict')
page-selector(mode='select', v-model='insertLinkDialog', :open-handler='insertLinkHandler', :path='path', :locale='locale')
</template>
<script>
import _ from 'lodash'
import { get, sync } from 'vuex-pathify'
import DecoupledEditor from '@requarks/ckeditor5'
// import DecoupledEditor from '../../../../wiki-ckeditor5/build/ckeditor'
import EditorConflict from './ckeditor/conflict.vue'
import { html as beautify } from 'js-beautify/js/lib/beautifier.min.js'
/* global siteLangs */
export default {
components: {
EditorConflict
},
props: {
save: {
type: Function,
default: () => {}
}
},
data() {
return {
editor: null,
stats: {
characters: 0,
words: 0
},
content: '',
isConflict: false,
insertLinkDialog: false
}
},
computed: {
isMobile() {
return this.$vuetify.breakpoint.smAndDown
},
locale: get('page/locale'),
path: get('page/path'),
activeModal: sync('editor/activeModal')
},
methods: {
insertLink () {
this.insertLinkDialog = true
},
insertLinkHandler ({ locale, path }) {
this.editor.execute('link', siteLangs.length > 0 ? `/${locale}/${path}` : `/${path}`)
}
},
async mounted () {
this.$store.set('editor/editorKey', 'ckeditor')
this.editor = await DecoupledEditor.create(this.$refs.editor, {
language: this.locale,
placeholder: 'Type the page content here',
disableNativeSpellChecker: false,
// TODO: Mention autocomplete
//
// mention: {
// feeds: [
// {
// marker: '@',
// feed: [ '@Barney', '@Lily', '@Marshall', '@Robin', '@Ted' ],
// minimumCharacters: 1
// }
// ]
// },
wordCount: {
onUpdate: stats => {
this.stats = {
characters: stats.characters,
words: stats.words
}
}
}
})
this.$refs.toolbarContainer.appendChild(this.editor.ui.view.toolbar.element)
if (this.mode !== 'create') {
this.editor.setData(this.$store.get('editor/content'))
}
this.editor.model.document.on('change:data', _.debounce(evt => {
this.$store.set('editor/content', beautify(this.editor.getData(), { indent_size: 2, end_with_newline: true }))
}, 300))
this.$root.$on('editorInsert', opts => {
switch (opts.kind) {
case 'IMAGE':
this.editor.execute('imageInsert', {
source: opts.path
})
break
case 'BINARY':
this.editor.execute('link', opts.path, {
linkIsDownloadable: true
})
break
case 'DIAGRAM':
this.editor.execute('imageInsert', {
source: `data:image/svg+xml;base64,${opts.text}`
})
break
}
})
this.$root.$on('editorLinkToPage', opts => {
this.insertLink()
})
// Handle save conflict
this.$root.$on('saveConflict', () => {
this.isConflict = true
})
this.$root.$on('overwriteEditorContent', () => {
this.editor.setData(this.$store.get('editor/content'))
})
},
beforeDestroy () {
if (this.editor) {
this.editor.destroy()
this.editor = null
}
}
}
</script>
<style lang="scss">
$editor-height: calc(100vh - 64px - 24px);
$editor-height-mobile: calc(100vh - 56px - 16px);
.editor-ckeditor {
background-color: mc('grey', '200');
flex: 1 1 50%;
display: flex;
flex-flow: column nowrap;
height: $editor-height;
max-height: $editor-height;
position: relative;
@at-root .theme--dark & {
background-color: mc('grey', '900');
}
@include until($tablet) {
height: $editor-height-mobile;
max-height: $editor-height-mobile;
}
&-sysbar {
padding-left: 0;
&-locale {
background-color: rgba(255,255,255,.25);
display:inline-flex;
padding: 0 12px;
height: 24px;
width: 63px;
justify-content: center;
align-items: center;
}
}
.contents {
table {
margin: inherit;
}
pre > code {
background-color: unset;
color: unset;
padding: .15em;
}
}
.ck.ck-toolbar {
border: none;
justify-content: center;
background-color: mc('grey', '300');
color: #FFF;
}
.ck.ck-toolbar__items {
justify-content: center;
}
> .ck-editor__editable {
background-color: mc('grey', '100');
overflow-y: auto;
overflow-x: hidden;
padding: 2rem;
box-shadow: 0 0 5px hsla(0, 0, 0, .1);
margin: 1rem auto 0;
width: calc(100vw - 256px - 16vw);
min-height: calc(100vh - 64px - 24px - 1rem - 40px);
border-radius: 5px;
@at-root .theme--dark & {
background-color: #303030;
color: #FFF;
}
@include until($widescreen) {
width: calc(100vw - 2rem);
margin: 1rem 1rem 0 1rem;
min-height: calc(100vh - 64px - 24px - 1rem - 40px);
}
@include until($tablet) {
width: 100%;
margin: 0;
min-height: calc(100vh - 56px - 24px - 76px);
}
&.ck.ck-editor__editable:not(.ck-editor__nested-editable).ck-focused {
border-color: #FFF;
box-shadow: 0 0 10px rgba(mc('blue', '700'), .25);
@at-root .theme--dark & {
border-color: #444;
border-bottom: none;
box-shadow: 0 0 10px rgba(#000, .25);
}
}
&.ck .ck-editor__nested-editable.ck-editor__nested-editable_focused,
&.ck .ck-editor__nested-editable:focus,
.ck-widget.table td.ck-editor__nested-editable.ck-editor__nested-editable_focused,
.ck-widget.table th.ck-editor__nested-editable.ck-editor__nested-editable_focused {
background-color: mc('grey', '100');
@at-root .theme--dark & {
background-color: mc('grey', '900');
}
}
}
}
</style>
<template lang='pug'>
v-card.editor-modal-blocks.animated.fadeInLeft(flat, tile)
v-container.pa-3(grid-list-lg, fluid)
v-row(dense)
v-col(
v-for='(item, idx) of blocks'
:key='`block-` + item.key'
cols='12'
lg='4'
xl='3'
)
v-card.radius-7(light, flat, @click='selectBlock(item)')
v-card-text
.d-flex.align-center
v-avatar.radius-7(color='teal')
v-icon(dark) {{item.icon}}
.pl-3
.body-2: strong.teal--text {{item.title}}
.caption.grey--text {{item.description}}
</template>
<script>
import _ from 'lodash'
import { sync } from 'vuex-pathify'
export default {
props: {
value: {
type: Boolean,
default: false
}
},
data() {
return {
blocks: [
{
key: 'childlist',
title: 'List Children Pages',
description: 'Display a links list of all children of this page.',
icon: 'mdi-format-list-text'
},
{
key: 'tabs',
title: 'Tabs',
description: 'Organize content within tabs.',
icon: 'mdi-tab'
}
]
}
},
computed: {
isShown: {
get() { return this.value },
set(val) { this.$emit('input', val) }
},
activeModal: sync('editor/activeModal')
},
methods: {
selectBlock (item) {
this.block = _.cloneDeep(item)
}
}
}
</script>
<style lang='scss'>
.editor-modal-blocks {
position: fixed;
top: 112px;
left: 64px;
z-index: 10;
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>
<template lang='pug'>
v-card.editor-modal-conflict.animated.fadeIn(flat, tile)
.pa-4
v-toolbar.radius-7(flat, color='indigo', style='border-bottom-left-radius: 0; border-bottom-right-radius: 0;', dark)
v-icon.mr-3 mdi-merge
.subtitle-1 {{$t('editor:conflict.title')}}
v-spacer
v-btn(outlined, color='white', @click='useLocal', :title='$t(`editor:conflict.useLocalHint`)')
v-icon(left) mdi-alpha-l-box
span {{$t('editor:conflict.useLocal')}}
v-dialog(
v-model='isRemoteConfirmDiagShown'
width='500'
)
template(v-slot:activator='{ on }')
v-btn.ml-3(outlined, color='white', v-on='on', :title='$t(`editor:conflict.useRemoteHint`)')
v-icon(left) mdi-alpha-r-box
span {{$t('editor:conflict.useRemote')}}
v-card
.dialog-header.is-short.is-indigo
v-icon.mr-3(color='white') mdi-alpha-r-box
span {{$t('editor:conflict.overwrite.title')}}
v-card-text.pa-4
i18next.body-2(tag='div', path='editor:conflict.overwrite.description')
strong(place='refEditsLost') {{$t('editor:conflict.overwrite.editsLost')}}
v-card-chin
v-spacer
v-btn(outlined, color='indigo', @click='isRemoteConfirmDiagShown = false')
v-icon(left) mdi-close
span {{$t('common:actions.cancel')}}
v-btn(@click='useRemote', color='indigo', dark)
v-icon(left) mdi-check
span {{$t('common:actions.confirm')}}
v-divider.mx-3(vertical)
v-btn(outlined, color='indigo lighten-4', @click='close')
v-icon(left) mdi-close
span {{$t('common:actions.cancel')}}
v-row.indigo.darken-1.body-2(no-gutters)
v-col.pa-4
v-icon.mr-3(color='white') mdi-alpha-l-box
i18next.white--text(tag='span', path='editor:conflict.localVersion')
em.indigo--text.text--lighten-4(place='refEditable') {{$t('editor:conflict.editable')}}
v-divider(vertical)
v-col.pa-4
v-icon.mr-3(color='white') mdi-alpha-r-box
i18next.white--text(tag='span', path='editor:conflict.remoteVersion')
em.indigo--text.text--lighten-4(place='refReadOnly') {{$t('editor:conflict.readonly')}}
v-row.grey.lighten-2.body-2(no-gutters)
v-col.px-4.py-2
i18next.grey--text.text--darken-2(tag='em', path='editor:conflict.leftPanelInfo')
span(place='date', :title='$options.filters.moment(checkoutDateActive, `LLL`)') {{ checkoutDateActive | moment('from') }}
v-divider(vertical)
v-col.px-4.py-2
i18next.grey--text.text--darken-2(tag='em', path='editor:conflict.rightPanelInfo')
strong(place='authorName') {{latest.authorName}}
span(place='date', :title='$options.filters.moment(latest.updatedAt, `LLL`)') {{ latest.updatedAt | moment('from') }}
v-row.grey.lighten-3.grey--text.text--darken-3(no-gutters)
v-col.pa-4
.body-2
strong.indigo--text {{$t('editor:conflict.pageTitle')}}
strong.pl-2 {{title}}
.caption
strong.indigo--text {{$t('editor:conflict.pageDescription')}}
span.pl-2 {{description}}
v-divider(vertical, light)
v-col.pa-4
.body-2
strong.indigo--text {{$t('editor:conflict.pageTitle')}}
strong.pl-2 {{latest.title}}
.caption
strong.indigo--text {{$t('editor:conflict.pageDescription')}}
span.pl-2 {{latest.description}}
v-card.radius-7(:light='!$vuetify.theme.dark', :dark='$vuetify.theme.dark')
div(ref='cm')
</template>
<script>
import _ from 'lodash'
import gql from 'graphql-tag'
import { sync, get } from 'vuex-pathify'
/* global siteConfig */
// ========================================
// IMPORTS
// ========================================
import '../../libs/codemirror-merge/diff-match-patch.js'
// Code Mirror
import CodeMirror from 'codemirror'
import 'codemirror/lib/codemirror.css'
// Language
import 'codemirror/mode/markdown/markdown.js'
import 'codemirror/mode/htmlmixed/htmlmixed.js'
// Addons
import 'codemirror/addon/selection/active-line.js'
import 'codemirror/addon/merge/merge.js'
import 'codemirror/addon/merge/merge.css'
export default {
data() {
return {
cm: null,
latest: {
title: '',
description: '',
updatedAt: '',
authorName: ''
},
isRemoteConfirmDiagShown: false
}
},
computed: {
editorKey: get('editor/editorKey'),
activeModal: sync('editor/activeModal'),
pageId: get('page/id'),
title: get('page/title'),
description: get('page/description'),
updatedAt: get('page/updatedAt'),
checkoutDateActive: sync('editor/checkoutDateActive')
},
methods: {
close () {
this.isRemoteConfirmDiagShown = false
this.activeModal = ''
},
overwriteAndClose() {
this.checkoutDateActive = this.latest.updatedAt
this.$root.$emit('overwriteEditorContent')
this.$root.$emit('resetEditorConflict')
this.close()
},
useLocal () {
this.$store.set('editor/content', this.cm.edit.getValue())
this.overwriteAndClose()
},
useRemote () {
this.$store.set('editor/content', this.latest.content)
this.overwriteAndClose()
}
},
async mounted () {
let textMode = 'text/html'
switch (this.editorKey) {
case 'markdown':
textMode = 'text/markdown'
break
}
let resp = await this.$apollo.query({
query: gql`
query ($id: Int!) {
pages {
conflictLatest(id: $id) {
id
authorId
authorName
content
createdAt
description
isPublished
locale
path
tags
title
updatedAt
}
}
}
`,
fetchPolicy: 'network-only',
variables: {
id: this.$store.get('page/id')
}
})
resp = _.get(resp, 'data.pages.conflictLatest', false)
if (!resp) {
return this.$store.commit('showNotification', {
message: 'Failed to fetch latest version.',
style: 'warning',
icon: 'warning'
})
}
this.latest = resp
this.cm = CodeMirror.MergeView(this.$refs.cm, {
value: this.$store.get('editor/content'),
orig: resp.content,
tabSize: 2,
mode: textMode,
lineNumbers: true,
lineWrapping: true,
connect: null,
highlightDifferences: true,
styleActiveLine: true,
collapseIdentical: true,
direction: siteConfig.rtl ? 'rtl' : 'ltr'
})
this.cm.rightOriginal().setSize(null, 'calc(100vh - 265px)')
this.cm.editor().setSize(null, 'calc(100vh - 265px)')
this.cm.wrap.style.height = 'calc(100vh - 265px)'
}
}
</script>
<style lang='scss'>
.editor-modal-conflict {
position: fixed !important;
top: 0;
left: 0;
z-index: 10;
width: 100%;
height: 100vh;
background-color: rgba(0, 0, 0, .9) !important;
overflow: auto;
}
</style>
<template lang='pug'>
v-card.editor-modal-drawio.animated.fadeIn(flat, tile)
iframe(
ref='drawio'
src='https://embed.diagrams.net/?embed=1&proto=json&spin=1&saveAndExit=1&noSaveBtn=1&noExitBtn=0'
frameborder='0'
)
</template>
<script>
import { sync, get } from 'vuex-pathify'
// const xmlTest = `<?xml version="1.0" encoding="UTF-8"?>
// <mxfile version="13.4.2">
// <diagram id="SgbkCjxR32CZT1FvBvkp" name="Page-1">
// <mxGraphModel dx="2062" dy="1123" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
// <root>
// <mxCell id="0" />
// <mxCell id="1" parent="0" />
// <mxCell id="5gE3BTvRYS_8FoJnOusC-1" value="" style="whiteSpace=wrap;html=1;aspect=fixed;fillColor=#f8cecc;strokeColor=#b85450;" vertex="1" parent="1">
// <mxGeometry x="380" y="530" width="80" height="80" as="geometry" />
// </mxCell>
// </root>
// </mxGraphModel>
// </diagram>
// </mxfile>
// `
export default {
data() {
return {
content: ''
}
},
computed: {
editorKey: get('editor/editorKey'),
activeModal: sync('editor/activeModal')
},
methods: {
close () {
this.activeModal = ''
},
overwriteAndClose() {
this.$root.$emit('overwriteEditorContent')
this.$root.$emit('resetEditorConflict')
this.close()
},
send (msg) {
this.$refs.drawio.contentWindow.postMessage(JSON.stringify(msg), '*')
},
receive (evt) {
if (evt.frame === null || evt.source !== this.$refs.drawio.contentWindow || evt.data.length < 1) {
return
}
try {
const msg = JSON.parse(evt.data)
switch (msg.event) {
case 'init': {
this.send({
action: 'load',
autosave: 0,
modified: 'unsavedChanges',
xml: this.$store.get('editor/activeModalData'),
title: this.$store.get('page/title')
})
this.$store.set('editor/activeModalData', null)
break
}
case 'save': {
if (msg.exit) {
this.send({
action: 'export',
format: 'xmlsvg'
})
}
break
}
case 'export': {
const svgDataStart = msg.data.indexOf('base64,') + 7
this.$root.$emit('editorInsert', {
kind: 'DIAGRAM',
text: msg.data.slice(svgDataStart)
// text: msg.xml.replace(/ agent="(.*?)"/, '').replace(/ host="(.*?)"/, '').replace(/ etag="(.*?)"/, '')
})
this.close()
break
}
case 'exit': {
this.close()
break
}
}
} catch (err) {
console.error(err)
}
}
},
async mounted () {
window.addEventListener('message', this.receive)
},
beforeDestroy () {
window.removeEventListener('message', this.receive)
}
}
</script>
<style lang='scss'>
.editor-modal-drawio {
position: fixed !important;
top: 0;
left: 0;
z-index: 10;
width: 100%;
height: 100vh;
background-color: rgba(255,255,255, 1) !important;
overflow: hidden;
> iframe {
width: 100%;
height: 100vh;
border: 0;
padding: 0;
background-color: #FFF;
}
}
</style>
<template lang='pug'>
v-dialog(v-model='isShown', persistent, max-width='700', no-click-animation)
v-btn(fab, fixed, bottom, right, color='grey darken-3', dark, @click='goBack', style='width: 50px;'): v-icon mdi-undo-variant
v-card.radius-7(color='blue darken-3', dark)
v-card-text.text-center.py-4
.subtitle-1.white--text {{$t('editor:select.title')}}
v-container(grid-list-lg, fluid)
v-layout(row, wrap, justify-center)
v-flex(xs4)
v-card.radius-7.animated.fadeInUp.wait-p2s(
hover
light
ripple
)
v-card-text.text-center(@click='selectEditor("code")')
img(src='/_assets-legacy/svg/editor-icon-code.svg', alt='Code', style='width: 36px;')
.body-2.primary--text.mt-2 Code
.caption.grey--text Raw HTML
v-flex(xs4)
v-card.radius-7.animated.fadeInUp.wait-p1s(
hover
light
ripple
)
v-card-text.text-center(@click='selectEditor("markdown")')
img(src='/_assets-legacy/svg/editor-icon-markdown.svg', alt='Markdown', style='width: 36px;')
.body-2.primary--text.mt-2 Markdown
.caption.grey--text Plain Text Formatting
v-flex(xs4)
v-card.radius-7.animated.fadeInUp.wait-p3s(
hover
light
ripple
)
v-card-text.text-center(@click='selectEditor("ckeditor")')
img(src='/_assets-legacy/svg/editor-icon-ckeditor.svg', alt='Visual Editor', style='width: 36px;')
.body-2.mt-2.primary--text Visual Editor
.caption.grey--text Rich-text WYSIWYG
</template>
<script>
import _ from 'lodash'
import { sync, get } from 'vuex-pathify'
export default {
props: {
value: {
type: Boolean,
default: false
}
},
data() {
return {
templateDialogIsShown: false
}
},
computed: {
isShown: {
get() { return this.value },
set(val) { this.$emit('input', val) }
},
currentEditor: sync('editor/editor'),
locale: get('page/locale'),
path: get('page/path')
},
methods: {
selectEditor (name) {
this.currentEditor = `editor${_.startCase(name)}`
this.isShown = false
},
goBack () {
window.history.go(-1)
},
fromTemplate () {
this.templateDialogIsShown = true
},
fromTemplateHandle ({ id }) {
this.templateDialogIsShown = false
this.isShown = false
this.$nextTick(() => {
window.location.assign(`/e/${this.locale}/${this.path}?from=${id}`)
})
}
}
}
</script>
<style lang='scss'>
</style>
<template lang="pug">
v-dialog(v-model='isShown', max-width='550')
v-card
.dialog-header.is-short.is-red
v-icon.mr-2(color='white') mdi-alert
span {{$t('editor:unsaved.title')}}
v-card-text.pt-4
.body-2 {{$t('editor:unsaved.body')}}
v-card-chin
v-spacer
v-btn(text, @click='isShown = false') {{$t('common:actions.cancel')}}
v-btn.px-4(color='red', @click='discard', dark) {{$t('common:actions.discardChanges')}}
</template>
<script>
export default {
props: {
value: {
type: Boolean,
default: false
}
},
data() {
return { }
},
computed: {
isShown: {
get() { return this.value },
set(val) { this.$emit('input', val) }
}
},
methods: {
async discard() {
this.isShown = false
this.$emit('discard', true)
}
}
}
</script>
<template lang='pug'>
.editor-redirect
.editor-redirect-main
.editor-redirect-editor
v-container.px-2.pt-1(fluid)
v-row(dense)
v-col(
cols='12'
lg='8'
offset-lg='2'
xl='6'
offset-xl='3'
)
v-card.pt-2
v-card-text
.pb-1
.subtitle-2.primary--text When a user reaches this page
.caption.grey--text.text--darken-1 and matches one of these rules...
v-timeline(dense)
v-slide-x-reverse-transition(group, hide-on-leave)
v-timeline-item(
key='cond-add-new'
hide-dot
)
v-btn(
color='primary'
@click=''
)
v-icon(left) mdi-plus
span Add Conditional Rule
v-timeline-item(
key='cond-none'
small
color='grey'
)
v-card.grey.lighten-5(flat)
v-card-text
.body-2: strong No conditional rule
em Add conditional rules to direct users to a different page based on their group.
v-timeline-item(
key='cond-rule-1'
small
color='primary'
)
v-card.blue-grey.lighten-5(flat)
v-card-text
.d-flex.align-center
.body-2: strong User is a member of any of these groups:
v-select.ml-3(
color='primary'
:items='groups'
item-text='name'
item-value='id'
multiple
solo
flat
hide-details
dense
chips
small-chips
)
v-divider.my-3
.d-flex.align-center
.body-2.mr-3 then redirect to
v-btn-toggle.mr-3(
v-model='fallbackMode'
mandatory
color='primary'
borderless
dense
)
v-btn.text-none(value='page') Page
v-btn.text-none(value='url') External URL
v-btn.mr-3(
v-if='fallbackMode === `page`'
color='primary'
)
v-icon(left) mdi-magnify
span Select Page...
v-text-field(
v-if='fallbackMode === `url`'
label='External URL'
outlined
hint='Required - Title of the API'
hide-details
v-model='fallbackUrl'
dense
single-line
)
v-divider.mb-5
.subtitle-2.primary--text Otherwise, redirect to...
.caption.grey--text.text--darken-1.pb-2 This fallback rule is mandatory and used if none of the conditional rules above applies.
.d-flex.align-center
v-btn-toggle.mr-3(
v-model='fallbackMode'
mandatory
color='primary'
borderless
dense
)
v-btn.text-none(value='page') Page
v-btn.text-none(value='url') External URL
v-btn.mr-3(
v-if='fallbackMode === `page`'
color='primary'
)
v-icon(left) mdi-magnify
span Select Page...
v-text-field(
v-if='fallbackMode === `url`'
label='External URL'
outlined
hint='Required - Title of the API'
hide-details
v-model='fallbackUrl'
dense
single-line
)
v-system-bar.editor-redirect-sysbar(dark, status, color='grey darken-3')
.caption.editor-redirect-sysbar-locale {{locale.toUpperCase()}}
.caption.px-3 /{{path}}
template(v-if='$vuetify.breakpoint.mdAndUp')
v-spacer
.caption Redirect
v-spacer
.caption 0 rules
</template>
<script>
import _ from 'lodash'
import gql from 'graphql-tag'
import { v4 as uuid } from 'uuid'
import { get, sync } from 'vuex-pathify'
export default {
data() {
return {
fallbackMode: 'page',
fallbackUrl: 'https://'
}
},
computed: {
isMobile() {
return this.$vuetify.breakpoint.smAndDown
},
locale: get('page/locale'),
path: get('page/path'),
mode: get('editor/mode'),
activeModal: sync('editor/activeModal')
},
methods: {
},
mounted() {
this.$store.set('editor/editorKey', 'redirect')
if (this.mode === 'create') {
this.$store.set('editor/content', '<h1>Title</h1>\n\n<p>Some text here</p>')
}
},
apollo: {
groups: {
query: gql`
{
groups {
list {
id
name
}
}
}
`,
fetchPolicy: 'network-only',
update: (data) => data.groups.list,
watchLoading (isLoading) {
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'editor-redirect-groups')
}
}
}
}
</script>
<style lang='scss'>
$editor-height: calc(100vh - 64px - 24px);
$editor-height-mobile: calc(100vh - 56px - 16px);
.editor-redirect {
&-main {
display: flex;
width: 100%;
}
&-editor {
background-color: darken(mc('grey', '100'), 4.5%);
flex: 1 1 50%;
display: block;
height: $editor-height;
position: relative;
@at-root .theme--dark & {
background-color: darken(mc('grey', '900'), 4.5%);
}
}
&-sidebar {
width: 200px;
}
&-sysbar {
padding-left: 0 !important;
&-locale {
background-color: rgba(255,255,255,.25);
display:inline-flex;
padding: 0 12px;
height: 24px;
width: 63px;
justify-content: center;
align-items: center;
}
}
}
</style>
// Header matching code by CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: https://codemirror.net/LICENSE
import CodeMirror from 'codemirror'
const maxDepth = 100
const codeBlockStartMatch = /^`{3}[a-zA-Z0-9]+$/
const codeBlockEndMatch = /^`{3}$/
CodeMirror.registerHelper('fold', 'markdown', function (cm, start) {
const firstLine = cm.getLine(start.line)
const lastLineNo = cm.lastLine()
let end
function isHeader(lineNo) {
const tokentype = cm.getTokenTypeAt(CodeMirror.Pos(lineNo, 0))
return tokentype && /\bheader\b/.test(tokentype)
}
function headerLevel(lineNo, line, nextLine) {
let match = line && line.match(/^#+/)
if (match && isHeader(lineNo)) return match[0].length
match = nextLine && nextLine.match(/^[=-]+\s*$/)
if (match && isHeader(lineNo + 1)) return nextLine[0] === '=' ? 1 : 2
return maxDepth
}
// -> CODE BLOCK
if (codeBlockStartMatch.test(cm.getLine(start.line))) {
end = start.line
let nextNextLine = cm.getLine(end + 1)
while (end < lastLineNo) {
if (codeBlockEndMatch.test(nextNextLine)) {
end++
break
}
end++
nextNextLine = cm.getLine(end + 1)
}
} else {
// -> HEADER
let nextLine = cm.getLine(start.line + 1)
const level = headerLevel(start.line, firstLine, nextLine)
if (level === maxDepth) return undefined
end = start.line
let nextNextLine = cm.getLine(end + 2)
while (end < lastLineNo) {
if (headerLevel(end + 1, nextLine, nextNextLine) <= level) break
++end
nextLine = nextNextLine
nextNextLine = cm.getLine(end + 2)
}
}
return {
from: CodeMirror.Pos(start.line, firstLine.length),
to: CodeMirror.Pos(end, cm.getLine(end).length)
}
})
const pako = require('pako')
// ------------------------------------
// Markdown - PlantUML Preprocessor
// ------------------------------------
module.exports = {
init (mdinst, conf) {
mdinst.use((md, opts) => {
const openMarker = opts.openMarker || '```plantuml'
const openChar = openMarker.charCodeAt(0)
const closeMarker = opts.closeMarker || '```'
const closeChar = closeMarker.charCodeAt(0)
const imageFormat = opts.imageFormat || 'svg'
const server = opts.server || 'https://plantuml.requarks.io'
md.block.ruler.before('fence', 'uml_diagram', (state, startLine, endLine, silent) => {
let nextLine
let markup
let params
let token
let i
let autoClosed = false
let start = state.bMarks[startLine] + state.tShift[startLine]
let max = state.eMarks[startLine]
// Check out the first character quickly,
// this should filter out most of non-uml blocks
//
if (openChar !== state.src.charCodeAt(start)) { return false }
// Check out the rest of the marker string
//
for (i = 0; i < openMarker.length; ++i) {
if (openMarker[i] !== state.src[start + i]) { return false }
}
markup = state.src.slice(start, start + i)
params = state.src.slice(start + i, max)
// Since start is found, we can report success here in validation mode
//
if (silent) { return true }
// Search for the end of the block
//
nextLine = startLine
for (;;) {
nextLine++
if (nextLine >= endLine) {
// unclosed block should be autoclosed by end of document.
// also block seems to be autoclosed by end of parent
break
}
start = state.bMarks[nextLine] + state.tShift[nextLine]
max = state.eMarks[nextLine]
if (start < max && state.sCount[nextLine] < state.blkIndent) {
// non-empty line with negative indent should stop the list:
// - ```
// test
break
}
if (closeChar !== state.src.charCodeAt(start)) {
// didn't find the closing fence
continue
}
if (state.sCount[nextLine] > state.sCount[startLine]) {
// closing fence should not be indented with respect of opening fence
continue
}
var closeMarkerMatched = true
for (i = 0; i < closeMarker.length; ++i) {
if (closeMarker[i] !== state.src[start + i]) {
closeMarkerMatched = false
break
}
}
if (!closeMarkerMatched) {
continue
}
// make sure tail has spaces only
if (state.skipSpaces(start + i) < max) {
continue
}
// found!
autoClosed = true
break
}
const contents = state.src
.split('\n')
.slice(startLine + 1, nextLine)
.join('\n')
// We generate a token list for the alt property, to mimic what the image parser does.
let altToken = []
// Remove leading space if any.
let alt = params ? params.slice(1) : 'uml diagram'
state.md.inline.parse(
alt,
state.md,
state.env,
altToken
)
var zippedCode = encode64(pako.deflate('@startuml\n' + contents + '\n@enduml', { to: 'string' }))
token = state.push('uml_diagram', 'img', 0)
// alt is constructed from children. No point in populating it here.
token.attrs = [ [ 'src', `${server}/${imageFormat}/${zippedCode}` ], [ 'alt', '' ], ['class', 'uml-diagram'] ]
token.block = true
token.children = altToken
token.info = params
token.map = [ startLine, nextLine ]
token.markup = markup
state.line = nextLine + (autoClosed ? 1 : 0)
return true
}, {
alt: [ 'paragraph', 'reference', 'blockquote', 'list' ]
})
md.renderer.rules.uml_diagram = md.renderer.rules.image
}, {
openMarker: conf.openMarker,
closeMarker: conf.closeMarker,
imageFormat: conf.imageFormat,
server: conf.server
})
}
}
function encode64 (data) {
let r = ''
for (let i = 0; i < data.length; i += 3) {
if (i + 2 === data.length) {
r += append3bytes(data.charCodeAt(i), data.charCodeAt(i + 1), 0)
} else if (i + 1 === data.length) {
r += append3bytes(data.charCodeAt(i), 0, 0)
} else {
r += append3bytes(data.charCodeAt(i), data.charCodeAt(i + 1), data.charCodeAt(i + 2))
}
}
return r
}
function append3bytes (b1, b2, b3) {
let c1 = b1 >> 2
let c2 = ((b1 & 0x3) << 4) | (b2 >> 4)
let c3 = ((b2 & 0xF) << 2) | (b3 >> 6)
let c4 = b3 & 0x3F
let r = ''
r += encode6bit(c1 & 0x3F)
r += encode6bit(c2 & 0x3F)
r += encode6bit(c3 & 0x3F)
r += encode6bit(c4 & 0x3F)
return r
}
function encode6bit(raw) {
let b = raw
if (b < 10) {
return String.fromCharCode(48 + b)
}
b -= 10
if (b < 26) {
return String.fromCharCode(65 + b)
}
b -= 26
if (b < 26) {
return String.fromCharCode(97 + b)
}
b -= 26
if (b === 0) {
return '-'
}
if (b === 1) {
return '_'
}
return '?'
}
import cash from 'cash-dom'
import _ from 'lodash'
export default {
format () {
for (let i = 1; i < 6; i++) {
cash(`.editor-markdown-preview-content h${i}.tabset`).each((idx, elm) => {
elm.innerHTML = 'Tabset ( rendered upon saving )'
cash(elm).nextUntil(_.times(i, t => `h${t + 1}`).join(', '), `h${i + 1}`).each((hidx, hd) => {
hd.classList.add('tabset-header')
cash(hd).nextUntil(_.times(i + 1, t => `h${t + 1}`).join(', ')).wrapAll('<div class="tabset-content"></div>')
})
})
}
}
}
<template lang='pug'>
v-app
.newpage
.newpage-content
img.animated.fadeIn(src='/_assets-legacy/svg/icon-delete-file.svg', alt='Not Found')
.headline {{ $t('newpage.title') }}
.subtitle-1.mt-3 {{ $t('newpage.subtitle') }}
v-btn.mt-5(:href='`/e/` + locale + `/` + path', x-large)
v-icon(left) mdi-plus
span {{ $t('newpage.create') }}
v-btn.mt-5(color='purple lighten-3', href='javascript:window.history.go(-1);', outlined)
v-icon(left) mdi-arrow-left
span {{ $t('newpage.goback') }}
</template>
<script>
export default {
props: {
locale: {
type: String,
default: 'en'
},
path: {
type: String,
default: 'home'
}
},
data() {
return { }
}
}
</script>
<style lang='scss'>
</style>
<template lang='pug'>
v-app
.notfound
.notfound-content
img.animated.fadeIn(src='/_assets-legacy/svg/icon-delete-file.svg', alt='Not Found')
.headline {{$t('notfound.title')}}
.subheading.mt-3 {{$t('notfound.subtitle')}}
v-btn.mt-5(color='red lighten-4', href='/', large, outlined)
v-icon(left) mdi-home
span {{$t('notfound.gohome')}}
</template>
<script>
export default {
data() {
return { }
}
}
</script>
<style lang='scss'>
</style>
<template lang='pug'>
v-app(:dark='$vuetify.theme.dark').source
nav-header
v-content
v-toolbar(color='primary', dark)
i18next.subheading(v-if='versionId > 0', path='common:page.viewingSourceVersion', tag='div')
strong(place='date', :title='$options.filters.moment(versionDate, `LLL`)') {{versionDate | moment('lll')}}
strong(place='path') /{{path}}
i18next.subheading(v-else, path='common:page.viewingSource', tag='div')
strong(place='path') /{{path}}
template(v-if='$vuetify.breakpoint.mdAndUp')
v-spacer
.caption.blue--text.text--lighten-3 {{$t('common:page.id', { id: pageId })}}
.caption.blue--text.text--lighten-3.ml-4(v-if='versionId > 0') {{$t('common:page.versionId', { id: versionId })}}
v-btn.ml-4(v-if='versionId > 0', depressed, color='blue darken-1', @click='goHistory')
v-icon mdi-history
v-btn.ml-4(depressed, color='blue darken-1', @click='goLive') {{$t('common:page.returnNormalView')}}
v-card(tile)
v-card-text
v-card.grey.radius-7(flat, :class='$vuetify.theme.dark ? `darken-4` : `lighten-4`')
v-card-text
pre
code
slot
nav-footer
notify
search-results
</template>
<script>
export default {
props: {
pageId: {
type: Number,
default: 0
},
locale: {
type: String,
default: 'en'
},
path: {
type: String,
default: 'home'
},
versionId: {
type: Number,
default: 0
},
versionDate: {
type: String,
default: ''
},
effectivePermissions: {
type: String,
default: ''
}
},
data() {
return {}
},
created () {
this.$store.commit('page/SET_ID', this.id)
this.$store.commit('page/SET_LOCALE', this.locale)
this.$store.commit('page/SET_PATH', this.path)
this.$store.commit('page/SET_MODE', 'source')
if (this.effectivePermissions) {
this.$store.set('page/effectivePermissions', JSON.parse(Buffer.from(this.effectivePermissions, 'base64').toString()))
}
},
methods: {
goLive() {
window.location.assign(`/${this.locale}/${this.path}`)
},
goHistory () {
window.location.assign(`/h/${this.locale}/${this.path}`)
}
}
}
</script>
<style lang='scss'>
.source {
pre > code {
box-shadow: none;
background-color: transparent;
color: mc('grey', '800');
font-family: 'Roboto Mono', sans-serif;
font-weight: 400;
font-size: 1rem;
@at-root .theme--dark.source pre > code {
background-color: mc('grey', '900');
color: mc('grey', '400');
}
&::before {
display: none;
}
}
}
</style>
<template lang='pug'>
v-app
.unauthorized
.unauthorized-content
img.animated.fadeIn(src='/_assets-legacy/svg/icon-delete-shield.svg', alt='Unauthorized')
.headline {{$t('unauthorized.title')}}
.subtitle-1.mt-3 {{$t('unauthorized.action.' + action)}}
v-btn.mt-5(href='/login', x-large)
v-icon(left) mdi-login
span {{$t('unauthorized.login')}}
v-btn.mt-5(color='red lighten-4', href='javascript:window.history.go(-1);', outlined)
v-icon(left) mdi-arrow-left
span {{$t('unauthorized.goback')}}
</template>
<script>
export default {
props: {
action: {
type: String,
default: 'view'
}
},
data() {
return { }
}
}
</script>
<style lang='scss'>
</style>
This diff was suppressed by a .gitattributes entry.
This diff was suppressed by a .gitattributes entry.
This diff was suppressed by a .gitattributes entry.
This diff was suppressed by a .gitattributes entry.
This diff was suppressed by a .gitattributes entry.
This diff was suppressed by a .gitattributes entry.
This diff was suppressed by a .gitattributes entry.
This diff was suppressed by a .gitattributes entry.
This diff was suppressed by a .gitattributes entry.
This diff was suppressed by a .gitattributes entry.
This diff was suppressed by a .gitattributes entry.
This diff was suppressed by a .gitattributes entry.
This diff was suppressed by a .gitattributes entry.
This diff was suppressed by a .gitattributes entry.
This diff was suppressed by a .gitattributes entry.
This diff was suppressed by a .gitattributes entry.
This diff was suppressed by a .gitattributes entry.
This diff was suppressed by a .gitattributes entry.
This diff was suppressed by a .gitattributes entry.
This diff was suppressed by a .gitattributes entry.
This diff was suppressed by a .gitattributes entry.
This diff was suppressed by a .gitattributes entry.
query($locale: String!, $namespace: String!) {
localization {
translations(locale:$locale, namespace:$namespace) {
key
value
}
}
}
mutation($id: Int!) {
pages {
delete(id: $id) {
responseResult {
succeeded
errorCode
slug
message
}
}
}
}
mutation($id: Int!, $destinationPath: String!, $destinationLocale: String!) {
pages {
move(id: $id, destinationPath: $destinationPath, destinationLocale: $destinationLocale) {
responseResult {
succeeded
errorCode
slug
message
}
}
}
}
query ($limit: Int, $orderBy: PageOrderBy, $orderByDirection: PageOrderByDirection, $tags: [String!], $locale: String) {
pages {
list(limit: $limit, orderBy: $orderBy, orderByDirection: $orderByDirection, tags: $tags, locale: $locale) {
id
locale
path
title
description
createdAt
updatedAt
tags
}
}
}
query ($query: String!) {
pages {
search(query:$query) {
results {
id
title
description
path
locale
}
suggestions
totalHits
}
}
}
query {
pages {
tags {
tag
title
}
}
}
query ($parent: Int!, $mode: PageTreeMode!, $locale: String!) {
pages {
tree(parent: $parent, mode: $mode, locale: $locale) {
id
path
title
isFolder
pageId
parent
}
}
}
mutation ($id: Int!) {
assets {
deleteAsset(id: $id) {
responseResult {
succeeded
errorCode
slug
message
}
}
}
}
mutation ($id: Int!, $filename: String!) {
assets {
renameAsset(id:$id, filename: $filename) {
responseResult {
succeeded
errorCode
slug
message
}
}
}
}
mutation ($parentFolderId: Int!, $slug: String!) {
assets {
createFolder(parentFolderId:$parentFolderId, slug: $slug) {
responseResult {
succeeded
errorCode
slug
message
}
}
}
}
query ($parentFolderId: Int!) {
assets {
folders(parentFolderId:$parentFolderId) {
id
name
slug
}
}
}
query ($folderId: Int!, $kind: AssetKind!) {
assets {
list(folderId:$folderId, kind: $kind) {
id
filename
ext
kind
mime
fileSize
createdAt
updatedAt
}
}
}
// =======================================
// Fetch polyfill
// =======================================
// Requirement: Safari 9 and below, IE 11 and below
if (!window.fetch) {
require('whatwg-fetch')
}
import filesize from 'filesize.js'
import _ from 'lodash'
/* global siteConfig */
const helpers = {
/**
* Convert bytes to humanized form
* @param {number} rawSize Size in bytes
* @returns {string} Humanized file size
*/
filesize (rawSize) {
return _.toUpper(filesize(rawSize))
},
/**
* Convert raw path to safe path
* @param {string} rawPath Raw path
* @returns {string} Safe path
*/
makeSafePath (rawPath) {
let rawParts = _.split(_.trim(rawPath), '/')
rawParts = _.map(rawParts, (r) => {
return _.kebabCase(_.deburr(_.trim(r)))
})
return _.join(_.filter(rawParts, (r) => { return !_.isEmpty(r) }), '/')
},
resolvePath (path) {
if (_.startsWith(path, '/')) { path = path.substring(1) }
return `${siteConfig.path}${path}`
},
/**
* Set Input Selection
* @param {DOMElement} input The input element
* @param {number} startPos The starting position
* @param {nunber} endPos The ending position
*/
setInputSelection (input, startPos, endPos) {
input.focus()
if (typeof input.selectionStart !== 'undefined') {
input.selectionStart = startPos
input.selectionEnd = endPos
} else if (document.selection && document.selection.createRange) {
// IE branch
input.select()
var range = document.selection.createRange()
range.collapse(true)
range.moveEnd('character', endPos)
range.moveStart('character', startPos)
range.select()
}
}
}
export default {
install(Vue) {
Vue.$helpers = helpers
Object.defineProperties(Vue.prototype, {
$helpers: {
get() {
return helpers
}
}
})
}
}
require('core-js/stable')
require('regenerator-runtime/runtime')
/* eslint-disable no-unused-expressions */
switch (window.document.documentElement.lang) {
case 'ar':
case 'fa':
import(/* webpackChunkName: "fonts-arabic" */ './scss/fonts/arabic.scss')
break
default:
import(/* webpackChunkName: "fonts-default" */ './scss/fonts/default.scss')
break
}
require('modernizr')
require('./scss/app.scss')
import(/* webpackChunkName: "theme" */ './themes/default/scss/app.scss')
import(/* webpackChunkName: "mdi" */ '@mdi/font/css/materialdesignicons.css')
require('./helpers/compatibility.js')
require('./client-app.js')
import(/* webpackChunkName: "theme" */ './themes/default/js/app.js')
const renderEm = (tokens, idx, opts, env, slf) => {
const token = tokens[idx];
if (token.markup === '_') {
token.tag = 'u';
}
return slf.renderToken(tokens, idx, opts);
}
module.exports = (md) => {
md.renderer.rules.em_open = renderEm;
md.renderer.rules.em_close = renderEm;
}
/*! modernizr 3.6.0 (Custom Build) | MIT *
* https://modernizr.com/download/?-setclasses !*/
!function(n,e,s){function o(n){var e=r.className,s=Modernizr._config.classPrefix||"";if(c&&(e=e.baseVal),Modernizr._config.enableJSClass){var o=new RegExp("(^|\\s)"+s+"no-js(\\s|$)");e=e.replace(o,"$1"+s+"js$2")}Modernizr._config.enableClasses&&(e+=" "+s+n.join(" "+s),c?r.className.baseVal=e:r.className=e)}function a(n,e){return typeof n===e}function i(){var n,e,s,o,i,l,r;for(var c in f)if(f.hasOwnProperty(c)){if(n=[],e=f[c],e.name&&(n.push(e.name.toLowerCase()),e.options&&e.options.aliases&&e.options.aliases.length))for(s=0;s<e.options.aliases.length;s++)n.push(e.options.aliases[s].toLowerCase());for(o=a(e.fn,"function")?e.fn():e.fn,i=0;i<n.length;i++)l=n[i],r=l.split("."),1===r.length?Modernizr[r[0]]=o:(!Modernizr[r[0]]||Modernizr[r[0]]instanceof Boolean||(Modernizr[r[0]]=new Boolean(Modernizr[r[0]])),Modernizr[r[0]][r[1]]=o),t.push((o?"":"no-")+r.join("-"))}}var t=[],f=[],l={_version:"3.6.0",_config:{classPrefix:"",enableClasses:!0,enableJSClass:!0,usePrefixes:!0},_q:[],on:function(n,e){var s=this;setTimeout(function(){e(s[n])},0)},addTest:function(n,e,s){f.push({name:n,fn:e,options:s})},addAsyncTest:function(n){f.push({name:null,fn:n})}},Modernizr=function(){};Modernizr.prototype=l,Modernizr=new Modernizr;var r=e.documentElement,c="svg"===r.nodeName.toLowerCase();i(),o(t),delete l.addTest,delete l.addAsyncTest;for(var u=0;u<Modernizr._q.length;u++)Modernizr._q[u]();n.Modernizr=Modernizr}(window,document);
\ No newline at end of file
/* PrismJS 1.11.0
http://prismjs.com/download.html?themes=prism-dark&languages=markup+css+clike+javascript+c+bash+basic+cpp+csharp+arduino+ruby+elixir+fsharp+go+graphql+handlebars+haskell+ini+java+json+kotlin+latex+less+makefile+markdown+matlab+nginx+objectivec+perl+php+powershell+pug+python+typescript+rust+scss+scala+smalltalk+sql+stylus+swift+vbnet+yaml&plugins=line-numbers */
/**
* prism.js Dark theme for JavaScript, CSS and HTML
* Based on the slides of the talk “/Reg(exp){2}lained/”
* @author Lea Verou
*/
code[class*="language-"],
pre[class*="language-"] {
color: white;
background: none;
text-shadow: 0 -.1em .2em black;
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
@media print {
code[class*="language-"],
pre[class*="language-"] {
text-shadow: none;
}
}
pre[class*="language-"],
:not(pre) > code[class*="language-"] {
background: hsl(30, 20%, 25%);
}
/* Code blocks */
pre[class*="language-"] {
padding: 1em;
margin: .5em 0;
overflow: auto;
border: .3em solid hsl(30, 20%, 40%);
border-radius: .5em;
box-shadow: 1px 1px .5em black inset;
}
/* Inline code */
:not(pre) > code[class*="language-"] {
padding: .15em .2em .05em;
border-radius: .3em;
border: .13em solid hsl(30, 20%, 40%);
box-shadow: 1px 1px .3em -.1em black inset;
white-space: normal;
}
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: hsl(30, 20%, 50%);
}
.token.punctuation {
opacity: .7;
}
.namespace {
opacity: .7;
}
.token.property,
.token.tag,
.token.boolean,
.token.number,
.token.constant,
.token.symbol {
color: hsl(350, 40%, 70%);
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.inserted {
color: hsl(75, 70%, 60%);
}
.token.operator,
.token.entity,
.token.url,
.language-css .token.string,
.style .token.string,
.token.variable {
color: hsl(40, 90%, 60%);
}
.token.atrule,
.token.attr-value,
.token.keyword {
color: hsl(350, 40%, 70%);
}
.token.regex,
.token.important {
color: #e90;
}
.token.important,
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}
.token.deleted {
color: red;
}
pre.line-numbers {
position: relative;
padding-left: 3.8em;
counter-reset: linenumber;
}
pre.line-numbers > code {
position: relative;
white-space: inherit;
}
.line-numbers .line-numbers-rows {
position: absolute;
pointer-events: none;
top: 0;
font-size: 100%;
left: -3.8em;
width: 3em; /* works for line-numbers below 1000 lines */
letter-spacing: -1px;
border-right: 1px solid #999;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.line-numbers-rows > span {
pointer-events: none;
display: block;
counter-increment: linenumber;
}
.line-numbers-rows > span:before {
content: counter(linenumber);
color: #999;
display: block;
padding-right: 0.8em;
text-align: right;
}
export default {
readyStates: [],
callbacks: [],
/**
* Check if event has been sent
*
* @param {String} evt Event name
* @returns {Boolean} True if fired
*/
isReady (evt) {
return this.readyStates.indexOf(evt) >= 0
},
/**
* Register a callback to be executed when event is sent
*
* @param {String} evt Event name to register to
* @param {Function} clb Callback function
* @param {Boolean} once If the callback should be called only once
*/
register (evt, clb, once) {
if (this.isReady(evt)) {
clb()
} else {
this.callbacks.push({
event: evt,
callback: clb,
once: false,
called: false
})
}
},
/**
* Register a callback to be executed only once when event is sent
*
* @param {String} evt Event name to register to
* @param {Function} clb Callback function
*/
registerOnce (evt, clb) {
this.register(evt, clb, true)
},
/**
* Set ready state and execute callbacks
*/
notify (evt) {
this.readyStates.push(evt)
this.callbacks.forEach(clb => {
if (clb.event === evt) {
if (clb.once && clb.called) {
return
}
clb.called = true
clb.callback()
}
})
},
/**
* Execute callback on DOM ready
*
* @param {Function} clb Callback function
*/
onDOMReady (clb) {
if (document.readyState === 'interactive' || document.readyState === 'complete' || document.readyState === 'loaded') {
clb()
} else {
document.addEventListener('DOMContentLoaded', clb)
}
}
}
import i18next from 'i18next'
import Backend from 'i18next-chained-backend'
import LocalStorageBackend from 'i18next-localstorage-backend'
import i18nextXHR from 'i18next-xhr-backend'
import VueI18Next from '@panter/vue-i18next'
import _ from 'lodash'
/* global siteConfig, graphQL */
import localeQuery from 'gql/common/common-localization-query-translations.gql'
export default {
VueI18Next,
init() {
i18next
.use(Backend)
.init({
backend: {
backends: [
LocalStorageBackend,
i18nextXHR
],
backendOptions: [
{
expirationTime: 1000 * 60 * 60 * 24 // 24h
},
{
loadPath: '{{lng}}/{{ns}}',
parse: (data) => data,
ajax: (url, opts, cb, data) => {
let ns = {}
return cb(ns, {status: '200'})
// let langParams = url.split('/')
// graphQL.query({
// query: localeQuery,
// variables: {
// locale: langParams[0],
// namespace: langParams[1]
// }
// }).then(resp => {
// let ns = {}
// if (_.get(resp, 'data.localization.translations', []).length > 0) {
// resp.data.localization.translations.forEach(entry => {
// _.set(ns, entry.key, entry.value)
// })
// }
// return cb(ns, {status: '200'})
// }).catch(err => {
// console.error(err)
// return cb(null, {status: '404'})
// })
}
}
]
},
defaultNS: 'common',
lng: siteConfig.lang,
load: 'currentOnly',
lowerCaseLng: true,
fallbackLng: siteConfig.lang,
ns: ['common', 'auth']
})
return new VueI18Next(i18next)
}
}
// Production steps of ECMA-262, Edition 6, 22.1.2.1
if (!Array.from) {
Array.from = (function () {
var toStr = Object.prototype.toString
var isCallable = function (fn) {
return typeof fn === 'function' || toStr.call(fn) === '[object Function]'
}
var toInteger = function (value) {
var number = Number(value)
if (isNaN(number)) { return 0 }
if (number === 0 || !isFinite(number)) { return number }
return (number > 0 ? 1 : -1) * Math.floor(Math.abs(number))
}
var maxSafeInteger = Math.pow(2, 53) - 1
var toLength = function (value) {
var len = toInteger(value)
return Math.min(Math.max(len, 0), maxSafeInteger)
}
// The length property of the from method is 1.
return function from (arrayLike/*, mapFn, thisArg */) {
// 1. Let C be the this value.
var C = this
// 2. Let items be ToObject(arrayLike).
var items = Object(arrayLike)
// 3. ReturnIfAbrupt(items).
if (arrayLike == null) {
throw new TypeError('Array.from requires an array-like object - not null or undefined')
}
// 4. If mapfn is undefined, then let mapping be false.
var mapFn = arguments.length > 1 ? arguments[1] : void undefined
var T
if (typeof mapFn !== 'undefined') {
// 5. else
// 5. a If IsCallable(mapfn) is false, throw a TypeError exception.
if (!isCallable(mapFn)) {
throw new TypeError('Array.from: when provided, the second argument must be a function')
}
// 5. b. If thisArg was supplied, let T be thisArg; else let T be undefined.
if (arguments.length > 2) {
T = arguments[2]
}
}
// 10. Let lenValue be Get(items, "length").
// 11. Let len be ToLength(lenValue).
var len = toLength(items.length)
// 13. If IsConstructor(C) is true, then
// 13. a. Let A be the result of calling the [[Construct]] internal method
// of C with an argument list containing the single item len.
// 14. a. Else, Let A be ArrayCreate(len).
var A = isCallable(C) ? Object(new C(len)) : new Array(len)
// 16. Let k be 0.
var k = 0
// 17. Repeat, while k < len… (also steps a - h)
var kValue
while (k < len) {
kValue = items[k]
if (mapFn) {
A[k] = typeof T === 'undefined' ? mapFn(kValue, k) : mapFn.call(T, kValue, k)
} else {
A[k] = kValue
}
k += 1
}
// 18. Let putStatus be Put(A, "length", len, true).
A.length = len
// 20. Return A.
return A
}
}())
}
@import "global";
@import "base/base";
@import "base/icons";
@import "base/animation";
@import '~vuescroll/dist/vuescroll.css';
@import '~katex/dist/katex.min.css';
@import '~diff2html/bundles/css/diff2html.min.css';
@import 'components/codemirror';
@import 'components/katex';
@import 'components/v-btn';
@import 'components/v-data-table';
@import 'components/v-dialog';
@import 'components/v-form';
@import 'components/v-tabs';
// @import '../libs/twemoji/twemoji-awesome';
// @import '../libs/prism/prism.css';
// @import '~vue-tour/dist/vue-tour.css';
// @import '~xterm/dist/xterm.css';
// @import 'node_modules/diff2html/dist/diff2html.min';
@import 'pages/new';
@import 'pages/notfound';
@import 'pages/unauthorized';
@import 'pages/welcome';
@import 'pages/error';
@import 'layout/_rtl';
$use-fade: true;
$use-zoom: true;
$use-bounce: true;
@import "~animate-sass/animate";
@for $i from 1 to 12 {
.wait-p#{$i}s {
animation-delay: $i * .1s !important;
}
}
html {
box-sizing: border-box;
height: 100%;
overflow-y: auto !important;
}
*, *:before, *:after {
box-sizing: inherit;
}
[v-cloak], .is-hidden {
display: none;
}
#root {
position: relative;
min-height: 100%;
&.is-fullscreen {
height: 100vh;
}
}
.v-application--wrap {
transition: all 1.2s ease;
transform-origin: 50% 50%;
// background-color: #FFF;
@at-root .theme--dark & {
background-color: mc('grey', '900');
}
}
#root .v-application {
.overline {
line-height: 1rem;
font-size: .625rem!important;
font-weight: 400;
letter-spacing: .1666666667em!important;
}
@for $i from 0 through 25 {
.radius-#{$i} {
border-radius: #{$i}px;
}
}
@for $i from 1 through 5 {
.grey.darken-2-d#{$i} {
background-color: darken(mc('grey', '700'), percentage($i/100)) !important;
border-color: darken(mc('grey', '700'), percentage($i/100)) !important;
}
.grey.darken-2-l#{$i} {
background-color: lighten(mc('grey', '700'), percentage($i/100)) !important;
border-color: lighten(mc('grey', '700'), percentage($i/100)) !important;
}
.grey.darken-3-d#{$i} {
background-color: darken(mc('grey', '800'), percentage($i/100)) !important;
border-color: darken(mc('grey', '800'), percentage($i/100)) !important;
}
.grey.darken-3-l#{$i} {
background-color: lighten(mc('grey', '800'), percentage($i/100)) !important;
border-color: lighten(mc('grey', '800'), percentage($i/100)) !important;
}
.grey.darken-4-d#{$i} {
background-color: darken(mc('grey', '900'), percentage($i/100)) !important;
border-color: darken(mc('grey', '900'), percentage($i/100)) !important;
}
.grey.darken-4-l#{$i} {
background-color: lighten(mc('grey', '900'), percentage($i/100)) !important;
border-color: lighten(mc('grey', '900'), percentage($i/100)) !important;
}
}
.grey.darken-5 {
background-color: #0C0C0C !important;
border-color: #0C0C0C !important;
}
.blue.darken-5 {
background-color: darken(mc('blue', '900'), 20%) !important;
border-color: darken(mc('blue', '900'), 20%) !important;
}
.indigo.darken-5 {
background-color: darken(mc('indigo', '900'), 10%) !important;
border-color: darken(mc('indigo', '900'), 10%) !important;
}
}
// @font-face {
// font-family: 'Material Icons';
// font-style: normal;
// font-weight: 400;
// src: local('Material Icons'),
// local('MaterialIcons-Regular'),
// url(/fonts/MaterialIcons-Regular.woff2) format('woff2'),
// url(/fonts/MaterialIcons-Regular.woff) format('woff');
// }
// .material-icons {
// font-family: 'Material Icons', sans-serif;
// font-weight: normal;
// font-style: normal;
// font-size: 24px; /* Preferred icon size */
// display: inline-flex;
// line-height: 1;
// text-transform: none;
// letter-spacing: normal;
// word-wrap: normal;
// white-space: nowrap;
// direction: ltr;
// /* Support for all WebKit browsers. */
// -webkit-font-smoothing: antialiased;
// /* Support for Safari and Chrome. */
// text-rendering: optimizeLegibility;
// /* Support for Firefox. */
// -moz-osx-font-smoothing: grayscale;
// /* Support for IE. */
// font-feature-settings: 'liga';
// }
.icons {
display: inline-block;
color: mc('grey', '800');
&.is-text {
display: inline-block;
width: 1em;
height: 1em;
vertical-align: middle;
position: relative;
top: -0.0625em;
stroke: none;
fill: none;
}
@each $size in 16,18,20,24,32,48,64,96,128 {
&.is-#{$size} {
width: #{$size}px;
height: #{$size}px;
}
}
&.has-right-pad {
margin-right: .5rem;
}
&.is-outlined {
stroke-width: 2px;
use {
fill: inherit;
stroke: mc('grey', '800');
}
}
}
.material-design-icon {
display: inline-flex;
}
$material-colors: (
'red': (
'50': #ffebee,
'100': #ffcdd2,
'200': #ef9a9a,
'300': #e57373,
'400': #ef5350,
'500': #f44336,
'600': #e53935,
'700': #d32f2f,
'800': #c62828,
'900': #b71c1c,
'a100': #ff8a80,
'a200': #ff5252,
'a400': #ff1744,
'a700': #d50000
),
'pink': (
'50': #fce4ec,
'100': #f8bbd0,
'200': #f48fb1,
'300': #f06292,
'400': #ec407a,
'500': #e91e63,
'600': #d81b60,
'700': #c2185b,
'800': #ad1457,
'900': #880e4f,
'a100': #ff80ab,
'a200': #ff4081,
'a400': #f50057,
'a700': #c51162
),
'purple': (
'50': #f3e5f5,
'100': #e1bee7,
'200': #ce93d8,
'300': #ba68c8,
'400': #ab47bc,
'500': #9c27b0,
'600': #8e24aa,
'700': #7b1fa2,
'800': #6a1b9a,
'900': #4a148c,
'a100': #ea80fc,
'a200': #e040fb,
'a400': #d500f9,
'a700': #aa00ff
),
'deep-purple': (
'50': #ede7f6,
'100': #d1c4e9,
'200': #b39ddb,
'300': #9575cd,
'400': #7e57c2,
'500': #673ab7,
'600': #5e35b1,
'700': #512da8,
'800': #4527a0,
'900': #311b92,
'a100': #b388ff,
'a200': #7c4dff,
'a400': #651fff,
'a700': #6200ea
),
'indigo': (
'50': #e8eaf6,
'100': #c5cae9,
'200': #9fa8da,
'300': #7986cb,
'400': #5c6bc0,
'500': #3f51b5,
'600': #3949ab,
'700': #303f9f,
'800': #283593,
'900': #1a237e,
'a100': #8c9eff,
'a200': #536dfe,
'a400': #3d5afe,
'a700': #304ffe
),
'blue': (
'50': #e3f2fd,
'100': #bbdefb,
'200': #90caf9,
'300': #64b5f6,
'400': #42a5f5,
'500': #2196f3,
'600': #1e88e5,
'700': #1976d2,
'800': #1565c0,
'900': #0d47a1,
'a100': #82b1ff,
'a200': #448aff,
'a400': #2979ff,
'a700': #2962ff
),
'light-blue': (
'50': #e1f5fe,
'100': #b3e5fc,
'200': #81d4fa,
'300': #4fc3f7,
'400': #29b6f6,
'500': #03a9f4,
'600': #039be5,
'700': #0288d1,
'800': #0277bd,
'900': #01579b,
'a100': #80d8ff,
'a200': #40c4ff,
'a400': #00b0ff,
'a700': #0091ea
),
'cyan': (
'50': #e0f7fa,
'100': #b2ebf2,
'200': #80deea,
'300': #4dd0e1,
'400': #26c6da,
'500': #00bcd4,
'600': #00acc1,
'700': #0097a7,
'800': #00838f,
'900': #006064,
'a100': #84ffff,
'a200': #18ffff,
'a400': #00e5ff,
'a700': #00b8d4
),
'teal': (
'50': #e0f2f1,
'100': #b2dfdb,
'200': #80cbc4,
'300': #4db6ac,
'400': #26a69a,
'500': #009688,
'600': #00897b,
'700': #00796b,
'800': #00695c,
'900': #004d40,
'a100': #a7ffeb,
'a200': #64ffda,
'a400': #1de9b6,
'a700': #00bfa5
),
'green': (
'50': #e8f5e9,
'100': #c8e6c9,
'200': #a5d6a7,
'300': #81c784,
'400': #66bb6a,
'500': #4caf50,
'600': #43a047,
'700': #388e3c,
'800': #2e7d32,
'900': #1b5e20,
'a100': #b9f6ca,
'a200': #69f0ae,
'a400': #00e676,
'a700': #00c853
),
'light-green': (
'50': #f1f8e9,
'100': #dcedc8,
'200': #c5e1a5,
'300': #aed581,
'400': #9ccc65,
'500': #8bc34a,
'600': #7cb342,
'700': #689f38,
'800': #558b2f,
'900': #33691e,
'a100': #ccff90,
'a200': #b2ff59,
'a400': #76ff03,
'a700': #64dd17
),
'lime': (
'50': #f9fbe7,
'100': #f0f4c3,
'200': #e6ee9c,
'300': #dce775,
'400': #d4e157,
'500': #cddc39,
'600': #c0ca33,
'700': #afb42b,
'800': #9e9d24,
'900': #827717,
'a100': #f4ff81,
'a200': #eeff41,
'a400': #c6ff00,
'a700': #aeea00
),
'yellow': (
'50': #fffde7,
'100': #fff9c4,
'200': #fff59d,
'300': #fff176,
'400': #ffee58,
'500': #ffeb3b,
'600': #fdd835,
'700': #fbc02d,
'800': #f9a825,
'900': #f57f17,
'a100': #ffff8d,
'a200': #ffff00,
'a400': #ffea00,
'a700': #ffd600
),
'amber': (
'50': #fff8e1,
'100': #ffecb3,
'200': #ffe082,
'300': #ffd54f,
'400': #ffca28,
'500': #ffc107,
'600': #ffb300,
'700': #ffa000,
'800': #ff8f00,
'900': #ff6f00,
'a100': #ffe57f,
'a200': #ffd740,
'a400': #ffc400,
'a700': #ffab00
),
'orange': (
'50': #fff3e0,
'100': #ffe0b2,
'200': #ffcc80,
'300': #ffb74d,
'400': #ffa726,
'500': #ff9800,
'600': #fb8c00,
'700': #f57c00,
'800': #ef6c00,
'900': #e65100,
'a100': #ffd180,
'a200': #ffab40,
'a400': #ff9100,
'a700': #ff6d00
),
'deep-orange': (
'50': #fbe9e7,
'100': #ffccbc,
'200': #ffab91,
'300': #ff8a65,
'400': #ff7043,
'500': #ff5722,
'600': #f4511e,
'700': #e64a19,
'800': #d84315,
'900': #bf360c,
'a100': #ff9e80,
'a200': #ff6e40,
'a400': #ff3d00,
'a700': #dd2c00
),
'brown': (
'50': #efebe9,
'100': #d7ccc8,
'200': #bcaaa4,
'300': #a1887f,
'400': #8d6e63,
'500': #795548,
'600': #6d4c41,
'700': #5d4037,
'800': #4e342e,
'900': #3e2723
),
'grey': (
'50': #fafafa,
'100': #f5f5f5,
'200': #eeeeee,
'300': #e0e0e0,
'400': #bdbdbd,
'500': #9e9e9e,
'600': #757575,
'700': #616161,
'800': #424242,
'900': #212121
),
'blue-grey': (
'50': #eceff1,
'100': #cfd8dc,
'200': #b0bec5,
'300': #90a4ae,
'400': #78909c,
'500': #607d8b,
'600': #546e7a,
'700': #455a64,
'800': #37474f,
'900': #263238,
'1000': #11171a
),
'theme': (
'primary': #1976D2,
'secondary': #424242,
'accent': #82B1FF,
'error': #FF5252,
'info': #2196F3,
'success': #4CAF50,
'warning': #FFC107
)
);
@function mc($color-name, $color-variant: '500') {
$color: map-get(map-get($material-colors, $color-name),$color-variant);
@if $color {
@return $color;
} @else {
// Libsass still doesn't seem to support @error
@warn "=> ERROR: COLOR NOT FOUND! <= | Your $color-name, $color-variant combination did not match any of the values in the $material-colors map.";
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment