Initial commit
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<li class="apps__item">
|
||||
<div class="apps__icon">
|
||||
<img :src="oAuthApp.icon_url || require('../../assets/icons/apps/default.svg')">
|
||||
</div>
|
||||
|
||||
<div class="apps__item-info">
|
||||
<span class="text is-small is-semibold apps__item-title">{{ oAuthApp.name }}</span>
|
||||
<span class="text is-small apps__item-description">{{ oAuthApp.description || 'No description provided' }}</span>
|
||||
</div>
|
||||
|
||||
<slot />
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'AppElement',
|
||||
props: {
|
||||
oAuthApp: Object
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'new-dashboard/styles/variables';
|
||||
|
||||
.apps {
|
||||
&__item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
padding: 28px 0;
|
||||
border-bottom: 1px solid $neutral--300;
|
||||
}
|
||||
|
||||
&__item-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
&__item-title,
|
||||
&__item-description {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
&__item-title {
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
&__item-description {
|
||||
color: $neutral--600;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
display: flex;
|
||||
position: relative;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
overflow: hidden;
|
||||
border: 1px solid $neutral--300;
|
||||
border-radius: 2px;
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<section class="apps">
|
||||
<div class="apps__title">
|
||||
<h2 class="text is-small is-semibold">{{ $t(`OAuthAppsPage.title`) }}</h2>
|
||||
|
||||
<router-link class="button button--small" :to="{ name: 'oauth_app_new' }">
|
||||
{{ $t(`OAuthAppsPage.newAppButton`) }}
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<Spinner v-if="isFetchingApps" class="apps__spinner"/>
|
||||
|
||||
<p v-if="!isFetchingApps && !hasOAuthApps" v-html="$t(`OAuthAppsPage.emptyDescription`)" class="text is-caption u-mt--32"></p>
|
||||
<div v-else class="apps__list">
|
||||
<ul>
|
||||
<AppElement v-for="oAuthApp in oAuthApps" :key="oAuthApp.id" :oAuthApp="oAuthApp">
|
||||
<router-link
|
||||
class="button button--outline button--edit"
|
||||
:to="{ name: 'oauth_app_edit', params: {id: oAuthApp.id } }">
|
||||
{{ $t('OAuthAppsPage.editButton') }}
|
||||
</router-link>
|
||||
</AppElement>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<p class="text is-small u-mt--20" v-html="$t(`OAuthAppsPage.description`)"></p>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import AppElement from './AppElement';
|
||||
import Spinner from '../Spinner';
|
||||
|
||||
export default {
|
||||
name: 'AppList',
|
||||
components: {
|
||||
AppElement,
|
||||
Spinner
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
isFetchingApps: state => state.oAuthApps.isFetching,
|
||||
oAuthApps: state => state.oAuthApps.list,
|
||||
error: state => state.oAuthApps.error,
|
||||
hasOAuthApps: state => !state.oAuthApps.isFetching && !!Object.keys(state.oAuthApps.list).length
|
||||
})
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style scoped lang="scss">
|
||||
@import 'new-dashboard/styles/variables';
|
||||
|
||||
.apps {
|
||||
&__spinner {
|
||||
margin: 120px auto;
|
||||
}
|
||||
|
||||
&__title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid $neutral--300;
|
||||
}
|
||||
}
|
||||
|
||||
.button--edit {
|
||||
align-self: center;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<ul class="app-tabs">
|
||||
<li class="app-tabs-item">
|
||||
<a :href="`${ baseUrl }/your_apps`" class="text is-small is-txtPrimary app-tabs-link">{{ $t(`SettingsPages.tabs.apiKeys`) }}</a>
|
||||
</li>
|
||||
<li class="app-tabs-item is-active">
|
||||
<router-link :to="{ name: 'oauth_apps_list' }" class="text is-small is-txtPrimary app-tabs-link">
|
||||
{{ $t(`SettingsPages.tabs.oAuthApps`) }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li class="app-tabs-item">
|
||||
<a :href="`${ baseUrl }/your_apps/mobile`" class="text is-small is-txtPrimary app-tabs-link">{{ $t(`SettingsPages.tabs.mobileApps`) }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: 'AppTabs',
|
||||
computed: {
|
||||
...mapState({
|
||||
baseUrl: state => state.user.base_url
|
||||
})
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'new-dashboard/styles/variables';
|
||||
|
||||
.app-tabs {
|
||||
display: flex;
|
||||
margin-bottom: 26px;
|
||||
border-bottom: 1px solid $neutral--300;
|
||||
}
|
||||
|
||||
.app-tabs-item {
|
||||
position: relative;
|
||||
margin-right: 32px;
|
||||
padding-bottom: 34px;
|
||||
transform: translate3d(0, 1px, 0);
|
||||
|
||||
&.is-active {
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background: $neutral--800;
|
||||
}
|
||||
|
||||
.app-tabs-link {
|
||||
color: $text__color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.app-tabs-link {
|
||||
&.is-active {
|
||||
color: $text__color;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<section>
|
||||
<div class="appform__title">
|
||||
<router-link :to="{ name: 'oauth_apps_list' }">
|
||||
<img class="oauthapps__back" svg-inline src="new-dashboard/assets/icons/apps/back-arrow.svg" />
|
||||
</router-link>
|
||||
<h2 class="text is-small is-semibold u-ml--12">{{ $t(`OAuthAppsPage.form.newTitle`) }}</h2>
|
||||
</div>
|
||||
|
||||
<FormComponent :oAuthApplication="app" :error="error" @submit="onSubmit">
|
||||
<div class="appform__toolbar">
|
||||
<div class="u-flex">
|
||||
<router-link :to="{ name: 'oauth_apps_list' }" class="button button--ghost u-mr--28">
|
||||
{{ $t(`OAuthAppsPage.form.cancelButton`) }}
|
||||
</router-link>
|
||||
|
||||
<button class="button button--primary" type="submit" value="Submit">
|
||||
{{ $t(`OAuthAppsPage.form.saveButton`) }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</FormComponent>
|
||||
|
||||
<DeleteAppModal ref="deleteAppModal" :app="app"/>
|
||||
<RegenerateCredentialsModal ref="regenerateCredentialsModal" :app="app"/>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FormComponent from './FormComponent';
|
||||
import DeleteAppModal from './modals/DeleteAppModal';
|
||||
import RegenerateCredentialsModal from './modals/RegenerateCredentialsModal';
|
||||
|
||||
export default {
|
||||
name: 'CreateApp',
|
||||
components: {
|
||||
FormComponent,
|
||||
DeleteAppModal,
|
||||
RegenerateCredentialsModal
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
app: {},
|
||||
error: {}
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onSubmit (app) {
|
||||
this.createApp(app);
|
||||
},
|
||||
createApp (app) {
|
||||
this.$store.dispatch('oAuthApps/create', app)
|
||||
.then(
|
||||
createdApp => {
|
||||
this.$router.push({name: 'oauth_app_edit', params: { id: createdApp.id }});
|
||||
},
|
||||
error => {
|
||||
this.error = error;
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import 'new-dashboard/styles/variables';
|
||||
|
||||
.appform__title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: start;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 28px;
|
||||
border-bottom: 1px solid $neutral--300;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,181 @@
|
||||
<template>
|
||||
<section>
|
||||
<div class="appform__title">
|
||||
<router-link :to="{ name: 'oauth_apps_list' }">
|
||||
<img class="oauthapps__back" svg-inline src="../../assets/icons/apps/back-arrow.svg" />
|
||||
</router-link>
|
||||
|
||||
<h2 class="text is-small is-semibold u-ml--12">
|
||||
{{ $t(`OAuthAppsPage.form.editTitle`)}}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<Spinner v-if="isFetchingOAuthApps" class="apps__spinner"/>
|
||||
|
||||
<section class="appform__content" v-if="!isFetchingOAuthApps">
|
||||
<div class="u-mb--64">
|
||||
<div class="u-mb--16">
|
||||
<span class="appform__label">{{ $t(`OAuthAppsPage.form.ownedby`) }}</span>
|
||||
<span class="text is-small">{{ app.username }}</span>
|
||||
</div>
|
||||
|
||||
<div class="u-mb--16">
|
||||
<span class="appform__label">{{ $t(`OAuthAppsPage.form.clientId`) }}</span>
|
||||
<span class="text is-small">{{ app.client_id }}</span>
|
||||
</div>
|
||||
|
||||
<div class="u-mb--16">
|
||||
<span class="appform__label">{{ $t(`OAuthAppsPage.form.clientSecret`) }}</span>
|
||||
<span class="text is-small">{{ app.client_secret }}</span>
|
||||
</div>
|
||||
|
||||
<div class="u-flex u-mb--24">
|
||||
<img src="../../assets/icons/apps/warning.svg" />
|
||||
<span class="text is-small is-txtSoftGrey u-ml--8">{{ $t(`OAuthAppsPage.form.clientSecretWarning`) }}</span>
|
||||
</div>
|
||||
|
||||
<button class="button button--outline u-mb--24" @click="openRegenerateCredentialsModal">
|
||||
{{ $t(`OAuthAppsPage.form.clientSecretButton`) }}
|
||||
</button>
|
||||
|
||||
<span class="text is-small is-txtSoftGrey" v-html="$t(`OAuthAppsPage.form.clientSecretDesc`)"></span>
|
||||
</div>
|
||||
|
||||
<div ref="formScroll" class="appform__title u-mb--24">
|
||||
<h2 class="text is-caption">{{ $t(`OAuthAppsPage.form.appInformationTitle`) }}</h2>
|
||||
</div>
|
||||
|
||||
<FormComponent
|
||||
v-if="!isFetchingOAuthApps"
|
||||
:oAuthApplication="app"
|
||||
:formTitle="$t(`OAuthAppsPage.form.editTitle`)"
|
||||
:error="error"
|
||||
@submit="onSubmit">
|
||||
<div class="appform__toolbar u-flex__justify--between">
|
||||
<button type="button" class="button button--outline button--delete u-mr--48" @click="openDeleteAppModal">
|
||||
{{ $t(`OAuthAppsPage.form.deleteAppButton`) }}
|
||||
</button>
|
||||
|
||||
<div class="u-flex">
|
||||
<router-link :to="{ name: 'oauth_apps_list' }" class="button button--ghost u-mr--28">
|
||||
{{ $t(`OAuthAppsPage.form.cancelButton`) }}
|
||||
</router-link>
|
||||
|
||||
<button class="button button--primary" type="submit" value="Submit">
|
||||
{{ $t(`OAuthAppsPage.form.saveButton`) }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</FormComponent>
|
||||
</section>
|
||||
|
||||
<DeleteAppModal ref="deleteAppModal" :app="app"/>
|
||||
<RegenerateCredentialsModal ref="regenerateCredentialsModal" :app="app"/>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import FormComponent from './FormComponent';
|
||||
import Spinner from '../Spinner';
|
||||
import DeleteAppModal from './modals/DeleteAppModal';
|
||||
import RegenerateCredentialsModal from './modals/RegenerateCredentialsModal';
|
||||
|
||||
export default {
|
||||
name: 'EditApp',
|
||||
components: {
|
||||
FormComponent,
|
||||
DeleteAppModal,
|
||||
RegenerateCredentialsModal,
|
||||
Spinner
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
error: {}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
list: state => state.oAuthApps.list,
|
||||
isFetchingOAuthApps: state => state.oAuthApps.isFetching,
|
||||
app (state) {
|
||||
if (this.isFetchingOAuthApps) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return this.list[this.$route.params.id];
|
||||
}
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
onSubmit (app) {
|
||||
return this.updateApp(app);
|
||||
},
|
||||
updateApp (app) {
|
||||
this.$store.dispatch('oAuthApps/update', app)
|
||||
.then(
|
||||
() => this.$router.push({ name: 'oauth_apps_list' }),
|
||||
(error) => {
|
||||
this.error = error;
|
||||
window.scrollTo(0, this.$refs.formScroll.offsetTop);
|
||||
}
|
||||
);
|
||||
},
|
||||
deleteApp () {
|
||||
this.$store.dispatch('oAuthApps/delete', this.app)
|
||||
.then(
|
||||
() => this.$router.push({ name: 'oauth_apps_list' })
|
||||
);
|
||||
},
|
||||
openDeleteAppModal () {
|
||||
this.$refs.deleteAppModal.open();
|
||||
},
|
||||
openRegenerateCredentialsModal () {
|
||||
this.$refs.regenerateCredentialsModal.open();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import 'new-dashboard/styles/variables';
|
||||
|
||||
.appform__title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: start;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 28px;
|
||||
border-bottom: 1px solid $neutral--300;
|
||||
}
|
||||
|
||||
.appform_spinner {
|
||||
margin: 120px auto;
|
||||
}
|
||||
|
||||
.appform__label {
|
||||
margin-bottom: 8px;
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 16px;
|
||||
|
||||
&--optional {
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
.appform__toolbar {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
height: 70px;
|
||||
border-top: 1px solid $neutral--400;
|
||||
}
|
||||
|
||||
.button--delete {
|
||||
border: 1px solid $red--600;
|
||||
color: $red--600;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,240 @@
|
||||
<template>
|
||||
<form id="app" @submit="onSubmit">
|
||||
<div class="appform__block">
|
||||
<label class="appform__label" for="app.name">{{ $t(`OAuthAppsPage.form.appName`) }}</label>
|
||||
|
||||
<input class="appform__input" :class="{'has-error': !!error.name}" type="text" name="app.name" id="app.name" v-model="app.name">
|
||||
<span v-if="error.name" class="appform__error u-mt--4">
|
||||
{{ getStringErrorFromArray(error.name, $t(`OAuthAppsPage.form.appName`)) }}
|
||||
</span>
|
||||
|
||||
<span class="appform__input-desc">{{ $t(`OAuthAppsPage.form.appNameDesc`) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="appform__block">
|
||||
<label class="appform__label" for="app.website_url">{{ $t(`OAuthAppsPage.form.webUrl`) }}</label>
|
||||
<input class="appform__input" type="text" name="app.website_url" id="app.website_url" v-model="app.website_url">
|
||||
<span v-if="error.website_url" class="appform__error u-mt--4">
|
||||
{{ getStringErrorFromArray(error.website_url, $t(`OAuthAppsPage.form.webUrl`)) }}
|
||||
</span>
|
||||
<span class="appform__input-desc">{{ $t(`OAuthAppsPage.form.webUrlDesc`) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="appform__block">
|
||||
<label class="appform__label" for="app.description">{{ $t(`OAuthAppsPage.form.description`) }}<span class="appform__label--optional"> {{ $t(`OAuthAppsPage.form.optional`) }}</span></label>
|
||||
<textarea class="appform__input appform__input--textarea" type="text" name="app.description" id="app.description" v-model="app.description"></textarea>
|
||||
<span class="appform__input-desc" v-html="$t(`OAuthAppsPage.form.descriptionDesc`)"></span>
|
||||
</div>
|
||||
|
||||
<div class="appform__block">
|
||||
<label class="appform__label" for="appCallbacks">{{ $t(`OAuthAppsPage.form.callbackUrls`) }}</label>
|
||||
<ul>
|
||||
<li v-for="(callbackUrl, k) in app.redirect_uris" :key="k">
|
||||
<input class="appform__input" :class="{'has-error': !!error.redirect_uris}" type="text" name="callbackUrl" id="callbackUrl" v-model="callbackUrl.name" :placeholder="$t(`OAuthAppsPage.form.callbackUrlPlaceholder`)">
|
||||
<span v-show="shouldShowDelete(k, app.redirect_uris)" class="appform__link u-ml--16 appform__link--delete" @click="removeCallbackUrl(k)">{{ $t(`OAuthAppsPage.form.callbackUrlDelete`) }}</span>
|
||||
<span v-show="k == app.redirect_uris.length-1" class="appform__link u-ml--16" @click="addCallbackUrl(k)">{{ $t(`OAuthAppsPage.form.callbackUrlAddMore`) }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<span v-if="error.redirect_uris" class="appform__error">
|
||||
{{ getStringErrorFromArray(error.redirect_uris, $t(`OAuthAppsPage.form.callbackUrls`)) }}
|
||||
</span>
|
||||
|
||||
<span class="appform__input-desc" v-html="$t(`OAuthAppsPage.form.callbackUrlsDesc`)"></span>
|
||||
</div>
|
||||
|
||||
<div class="appform__block">
|
||||
<label class="appform__label" for="app.logoUrl">
|
||||
{{ $t(`OAuthAppsPage.form.logoUpload`) }}<span class="appform__label--optional"> {{ $t(`OAuthAppsPage.form.optional`) }}</span>
|
||||
</label>
|
||||
|
||||
<UploadImage :currentImage="app.icon_url" @imageUpload="onImageUploaded">
|
||||
<p class="appform__input-desc appform__input-desc--12" v-html="$t(`OAuthAppsPage.form.logoUploadDesc`)"></p>
|
||||
<p v-if="error.icon_url" class="appform__error u-mt--4">
|
||||
{{ getStringErrorFromArray(error.icon_url, $t(`OAuthAppsPage.form.logo`)) }}
|
||||
</p>
|
||||
</UploadImage>
|
||||
</div>
|
||||
|
||||
<slot />
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import UploadImage from '../UploadImage';
|
||||
|
||||
export default {
|
||||
name: 'FormComponent',
|
||||
components: {
|
||||
UploadImage
|
||||
},
|
||||
props: {
|
||||
oAuthApplication: {
|
||||
type: Object,
|
||||
default () {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
error: {
|
||||
type: Object,
|
||||
default () {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
oAuthApplication (newApplication) {
|
||||
this.app = newApplication;
|
||||
this.app.redirect_uris = this.redirectUrisToArrayObjects(this.app.redirect_uris);
|
||||
}
|
||||
},
|
||||
data () {
|
||||
const app = {
|
||||
redirect_uris: [{ name: '' }],
|
||||
...this.$props.oAuthApplication
|
||||
};
|
||||
|
||||
if (this.$props.oAuthApplication &&
|
||||
this.$props.oAuthApplication.redirect_uris) {
|
||||
app.redirect_uris = this.redirectUrisToArrayObjects(this.$props.oAuthApplication.redirect_uris);
|
||||
}
|
||||
|
||||
return { app };
|
||||
},
|
||||
methods: {
|
||||
getStringErrorFromArray (errorArray, fieldName) {
|
||||
const errors = errorArray.map(error => (`${fieldName} ${error}.`));
|
||||
return errors.join(' ');
|
||||
},
|
||||
shouldShowDelete (index) {
|
||||
return index > 0;
|
||||
},
|
||||
onSubmit (event) {
|
||||
event.preventDefault();
|
||||
|
||||
const app = {
|
||||
...this.app,
|
||||
redirect_uris: this.redirectUrisToArrayStrings(this.app.redirect_uris)
|
||||
};
|
||||
|
||||
this.$emit('submit', app);
|
||||
},
|
||||
addCallbackUrl (index) {
|
||||
this.app.redirect_uris.push({ name: '' });
|
||||
},
|
||||
removeCallbackUrl (index) {
|
||||
this.app.redirect_uris.splice(index, 1);
|
||||
},
|
||||
redirectUrisToArrayObjects (redirectUris) {
|
||||
return redirectUris.map(r => ({ name: r }));
|
||||
},
|
||||
redirectUrisToArrayStrings (redirectUris) {
|
||||
return redirectUris.map(r => r.name).filter(Boolean);
|
||||
},
|
||||
onImageUploaded (imageSrc) {
|
||||
this.app.icon_url = imageSrc;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import 'new-dashboard/styles/variables';
|
||||
|
||||
.appform__title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: start;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 28px;
|
||||
border-bottom: 1px solid $neutral--300;
|
||||
}
|
||||
|
||||
.appform__block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.appform__label,
|
||||
.appform__input {
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.appform__label {
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
|
||||
&--optional {
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
.appform__input {
|
||||
width: 300px;
|
||||
margin-bottom: 6px;
|
||||
padding: 12px;
|
||||
border: 1px solid $neutral--400;
|
||||
border-radius: 4px;
|
||||
|
||||
&.has-error {
|
||||
border: 1px solid $danger__color;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border: 1px solid $blue--700;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border: 1px solid $neutral--400;
|
||||
}
|
||||
|
||||
&--textarea {
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
&-desc {
|
||||
color: $neutral--600;
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
font-size: 10px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
&-desc--12 {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.appform__link {
|
||||
color: $color-primary;
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
font-size: 12px;
|
||||
line-height: 22px;
|
||||
cursor: pointer;
|
||||
|
||||
&--delete {
|
||||
color: $danger__color;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.appform__toolbar {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
height: 70px;
|
||||
border-top: 1px solid $neutral--400;
|
||||
}
|
||||
|
||||
.appform__error {
|
||||
color: $danger__color;
|
||||
font-size: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<ul class="settingssidebar">
|
||||
<li class="settingssidebar-item">
|
||||
<a :href="`${ baseUrl }/profile`" class="text is-txtPrimary settingssidebar-link">{{ $t(`SettingsPages.sidebar.profile`) }}</a>
|
||||
</li>
|
||||
<li class="settingssidebar-item">
|
||||
<a :href="`${ baseUrl }/account`" class="text is-txtPrimary settingssidebar-link">{{ $t(`SettingsPages.sidebar.account`) }}</a>
|
||||
</li>
|
||||
<li class="settingssidebar-item">
|
||||
<router-link :to="{ name: 'connected_apps' }" class="text is-txtPrimary settingssidebar-link" :class="{'is-active': isConnectedAppsPage()}">{{ $t(`SettingsPages.sidebar.connectedApps`) }} </router-link>
|
||||
</li>
|
||||
<!--
|
||||
<li v-if="!isLocallyHosted && (isOrgAdmin || !isInsideOrg)" class="settingssidebar-item">
|
||||
<a :href="planUrl" class="text is-txtPrimary settingssidebar-link">{{ $t(`SettingsPages.sidebar.billing`) }}</a>
|
||||
</li>
|
||||
-->
|
||||
<li v-if="isOrgAdmin" class="settingssidebar-item">
|
||||
<a :href="`${ baseUrl }/organization`" class="text is-txtPrimary settingssidebar-link">{{ $t(`SettingsPages.sidebar.organizationSettings`) }}</a>
|
||||
</li>
|
||||
<span class="settingssidebar-separator"></span>
|
||||
<li class="settingssidebar-item">
|
||||
<a :href="`${ baseUrl }/your_apps`" class="text is-txtPrimary settingssidebar-link" :class="{'is-active': isDevelopersSettingsPage()}">{{ $t(`SettingsPages.sidebar.developerSettings`) }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: 'SettingsSidebar',
|
||||
data () {
|
||||
return {
|
||||
developerSettingsPages: [
|
||||
'oauth_apps',
|
||||
'oauth_app_new',
|
||||
'oauth_app_edit',
|
||||
'oauth_apps_list'
|
||||
]
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
baseUrl: state => state.user.base_url,
|
||||
user: state => state.user,
|
||||
planUrl: state => state.config.plan_url,
|
||||
isLocallyHosted: state => state.config.cartodb_com_hosted
|
||||
}),
|
||||
isInsideOrg () {
|
||||
return this.$store.getters['user/isOrganizationUser'];
|
||||
},
|
||||
isOrgAdmin () {
|
||||
return this.user.org_admin;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
isConnectedAppsPage () {
|
||||
return (this.$route || {}).name === 'connected_apps';
|
||||
},
|
||||
isDevelopersSettingsPage () {
|
||||
return this.developerSettingsPages.includes((this.$route || {}).name);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'new-dashboard/styles/variables';
|
||||
|
||||
.settingssidebar-item {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.settingssidebar-link {
|
||||
font-size: 14px;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
color: #333;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.settingssidebar-separator {
|
||||
display: flex;
|
||||
width: 240px;
|
||||
height: 1px;
|
||||
margin: 20px 0;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<Modal ref="deleteAppModal" name="deleteAppModal">
|
||||
<div class="oauthapps__modal">
|
||||
<div class="oauthapps__modal-inner">
|
||||
<div class="oauthapps__icon u-mb--24">
|
||||
<img class="oauthapps_svgicon" svg-inline src="new-dashboard/assets/icons/apps/default.svg">
|
||||
<img class="oauthapps__badge" svg-inline src="new-dashboard/assets/icons/apps/trash.svg">
|
||||
</div>
|
||||
<p class="text is-caption u-mb--8" v-html="$t('OAuthAppsPage.deleteModal.title', { name: app.name })"></p>
|
||||
<p class="text is-small is-txtSoftGrey" v-html="$t('OAuthAppsPage.deleteModal.subtitle')"></p>
|
||||
<div class="oauthapps__modal-actions">
|
||||
<button class="oauthapps__button button button--ghost u-mr--12" @click="close">{{ $t(`OAuthAppsPage.deleteModal.cancelButton`) }}</button>
|
||||
<button class="oauthapps__modal-button oauthapps__modal-button--delete" @click="deleteApp">{{ $t(`OAuthAppsPage.deleteModal.deleteButton`) }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Modal from 'new-dashboard/components/Modal';
|
||||
|
||||
export default {
|
||||
name: 'DeleteAppModal',
|
||||
components: {
|
||||
Modal
|
||||
},
|
||||
props: {
|
||||
app: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open () {
|
||||
this.$refs.deleteAppModal.open();
|
||||
},
|
||||
close () {
|
||||
this.$refs.deleteAppModal.close();
|
||||
},
|
||||
deleteApp () {
|
||||
this.$store.dispatch('oAuthApps/delete', this.app)
|
||||
.then(() => {
|
||||
this.$router.push({ name: 'oauth_apps_list' });
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import 'new-dashboard/styles/variables';
|
||||
@import './modal-styles.scss';
|
||||
</style>
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<Modal ref="regenerateCredentialsModal" name="regenerateCredentialsModal">
|
||||
<div class="oauthapps__modal">
|
||||
<div class="oauthapps__modal-inner">
|
||||
<div class="oauthapps__icon u-mb--24">
|
||||
<img class="oauthapps_svgicon" svg-inline src="new-dashboard/assets/icons/apps/default.svg">
|
||||
<img class="oauthapps__badge" svg-inline src="new-dashboard/assets/icons/apps/key.svg">
|
||||
</div>
|
||||
<p class="text is-caption u-mb--8" v-html="$t('OAuthAppsPage.regenerateModal.title', { name: app.name })"></p>
|
||||
<p class="text is-small is-txtSoftGrey" v-html="$t('OAuthAppsPage.regenerateModal.subtitle')"></p>
|
||||
<div class="oauthapps__modal-actions">
|
||||
<button class="oauthapps__modal-button u-mr--12" @click="close">{{ $t(`OAuthAppsPage.regenerateModal.cancelButton`) }}</button>
|
||||
<button class="oauthapps__modal-button oauthapps__modal-button--regenerate" @click="regenerateCredentials">{{ $t(`OAuthAppsPage.regenerateModal.regenerateButton`) }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Modal from 'new-dashboard/components/Modal';
|
||||
|
||||
export default {
|
||||
name: 'RegenerateCredentialsModal',
|
||||
components: {
|
||||
Modal
|
||||
},
|
||||
props: {
|
||||
app: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open () {
|
||||
this.$refs.regenerateCredentialsModal.open();
|
||||
},
|
||||
close () {
|
||||
this.$refs.regenerateCredentialsModal.close();
|
||||
},
|
||||
regenerateCredentials () {
|
||||
this.$store.dispatch('oAuthApps/regenerateCredentials', this.app);
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import 'new-dashboard/styles/variables';
|
||||
@import './modal-styles.scss';
|
||||
</style>
|
||||
@@ -0,0 +1,78 @@
|
||||
.oauthapps__modal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.oauthapps__icon {
|
||||
display: flex;
|
||||
position: relative;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border: 1px solid $neutral--400;
|
||||
border-radius: 2px;
|
||||
|
||||
.oauthapps_svgicon {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.oauthapps__badge {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: -8px;
|
||||
}
|
||||
}
|
||||
|
||||
.oauthapps__modal-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 960px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.oauthapps__modal-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
margin-top: 54px;
|
||||
padding-top: 38px;
|
||||
border-top: 1px solid $neutral--300;
|
||||
}
|
||||
|
||||
.oauthapps__modal-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 12px 18px;
|
||||
border: 1px solid $color-primary;
|
||||
border-radius: 4px;
|
||||
background-color: transparent;
|
||||
color: $color-primary;
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0;
|
||||
text-transform: uppercase;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: transparent;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
&--regenerate,
|
||||
&--delete {
|
||||
border: none;
|
||||
background-color: $button-alert__bg-color;
|
||||
color: $white;
|
||||
|
||||
&:hover {
|
||||
background-color: $button-alert__bg-color--hover;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<section class="BackgroundPolling" ref="injectionHTMLElement">
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BackgroundPollingView from 'dashboard/views/dashboard/background-polling/background-polling-view';
|
||||
|
||||
export default {
|
||||
name: 'BackgroundPollingView',
|
||||
props: {
|
||||
routeType: {
|
||||
type: String,
|
||||
default: 'maps'
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.backgroundPollingView = this.renderView();
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.cleanView();
|
||||
},
|
||||
methods: {
|
||||
getBackgroundPollingView () {
|
||||
return this.backgroundPollingView;
|
||||
},
|
||||
renderView () {
|
||||
const backgroundPollingModel = this.$cartoModels.backgroundPolling;
|
||||
const configModel = this.$cartoModels.config;
|
||||
const userModel = this.$cartoModels.user;
|
||||
|
||||
const backgroundPollingView = new BackgroundPollingView({
|
||||
model: backgroundPollingModel,
|
||||
createVis: this.$props.routeType === 'maps',
|
||||
userModel,
|
||||
configModel,
|
||||
el: this.$refs.injectionHTMLElement
|
||||
});
|
||||
|
||||
backgroundPollingView.render();
|
||||
backgroundPollingView.model.on('change', model => {
|
||||
if (model.get('state') === 'complete') {
|
||||
if (this.$props.routeType === 'home') {
|
||||
this.$store.dispatch('recentContent/fetch');
|
||||
this.$store.dispatch('maps/fetch');
|
||||
this.$store.dispatch('datasets/fetch');
|
||||
}
|
||||
|
||||
if (this.$props.routeType === 'maps') {
|
||||
this.$store.dispatch('maps/fetch');
|
||||
}
|
||||
|
||||
if (this.$props.routeType === 'datasets') {
|
||||
this.$store.dispatch('datasets/fetch');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return backgroundPollingView;
|
||||
},
|
||||
cleanView () {
|
||||
this.backgroundPollingView.clean();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<div class="Dialog is-white">
|
||||
<button class="CDB-Shape Dialog-closeBtn" @click="close">
|
||||
<div class="CDB-Shape-close is-blue is-huge"></div>
|
||||
</button>
|
||||
|
||||
<div class="Dialog-contentWrapper Dialog-contentWrapper--withHeaderWrapper">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Dialog',
|
||||
methods: {
|
||||
close () {
|
||||
this.$emit('close');
|
||||
}
|
||||
},
|
||||
beforeMount () {
|
||||
document.body.classList.add('u-overflow-hidden');
|
||||
},
|
||||
beforeDestroy () {
|
||||
document.body.classList.remove('u-overflow-hidden');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.Dialog .Dialog-header {
|
||||
box-sizing: content-box;
|
||||
|
||||
* {
|
||||
box-sizing: content-box;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<div class="Dialog-content Dialog-content--expanded" ref="injectionHTMLElement"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ChangePrivacyView from 'dashboard/views/dashboard/dialogs/change-privacy/change-privacy-view';
|
||||
import VisualizationModel from 'dashboard/data/visualization-model';
|
||||
import Dialog from 'new-dashboard/components/Backbone/Dialog';
|
||||
import ModalModel from 'new-dashboard/plugins/backbone/modal-model';
|
||||
|
||||
export default {
|
||||
name: 'ChangePrivacy',
|
||||
props: {
|
||||
visualization: Object
|
||||
},
|
||||
mounted () {
|
||||
this.dialog = this.renderDialog();
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.dialog.clean();
|
||||
},
|
||||
methods: {
|
||||
renderDialog () {
|
||||
const modalModel = ModalModel({
|
||||
create: createModalView => this.openDialog(createModalView),
|
||||
destroy: () => this.$emit('close')
|
||||
});
|
||||
|
||||
const visModel = new VisualizationModel(this.$props.visualization, {
|
||||
configModel: this.$cartoModels.config
|
||||
});
|
||||
|
||||
visModel.on('change', model => {
|
||||
this.$store.dispatch('user/updateData');
|
||||
this.$emit('updateVisualization', model);
|
||||
this.$emit('deselectAll');
|
||||
});
|
||||
|
||||
const changePrivacyView = new ChangePrivacyView({
|
||||
visModel,
|
||||
userModel: this.$cartoModels.user,
|
||||
configModel: this.$cartoModels.config,
|
||||
modals: modalModel,
|
||||
modalModel,
|
||||
el: this.$refs.injectionHTMLElement
|
||||
});
|
||||
|
||||
changePrivacyView._onShareClose = () => {};
|
||||
changePrivacyView.render();
|
||||
|
||||
return changePrivacyView;
|
||||
},
|
||||
|
||||
openDialog (createModalView) {
|
||||
const modalModel = {
|
||||
destroy: () => {}
|
||||
};
|
||||
|
||||
const modalView = createModalView(modalModel);
|
||||
modalView.render();
|
||||
|
||||
this.$modal.show({
|
||||
template: `
|
||||
<Dialog @close="$emit('close')">
|
||||
<div ref="dialogInjectionNode"></div>
|
||||
</Dialog>`,
|
||||
components: { Dialog },
|
||||
mounted: function () {
|
||||
// Replace injection node with Dialog
|
||||
const reference = this.$refs.dialogInjectionNode;
|
||||
const parentNode = reference.parentNode;
|
||||
parentNode.replaceChild(modalView.el, reference);
|
||||
|
||||
// Hack to be able to close Share Modal
|
||||
modalModel.destroy = () => {
|
||||
this.$emit('close');
|
||||
};
|
||||
}
|
||||
}, {}, { width: '100%', height: '100%' });
|
||||
|
||||
return modalModel;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.Dialog {
|
||||
.OptionCards {
|
||||
box-sizing: content-box;
|
||||
|
||||
* {
|
||||
box-sizing: content-box;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<div class="Dialog-content Dialog-content--expanded CreateDialog--new-dashboard" ref="injectionHTMLElement"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DialogView from 'dashboard/views/dashboard/dialogs/create-dialog/dialog-view';
|
||||
import ModalModel from 'new-dashboard/plugins/backbone/modal-model';
|
||||
import VisualizationModel from 'dashboard/data/visualization-model';
|
||||
|
||||
export default {
|
||||
name: 'CreateDialog',
|
||||
props: {
|
||||
backgroundPollingView: Object,
|
||||
mamufasImportView: Object,
|
||||
dialogType: {
|
||||
type: String,
|
||||
default: 'maps'
|
||||
},
|
||||
selectedItems: Array
|
||||
},
|
||||
mounted () {
|
||||
this.dialog = this.renderDialog();
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.dialog.clean();
|
||||
},
|
||||
methods: {
|
||||
renderDialog () {
|
||||
const configModel = this.$cartoModels.config;
|
||||
const userModel = this.$cartoModels.user;
|
||||
const backgroundPollingModel = this.$cartoModels.backgroundPolling;
|
||||
|
||||
let selectedItems = [];
|
||||
if (this.$props.selectedItems && this.$props.selectedItems.length) {
|
||||
selectedItems = this.$props.selectedItems.map(
|
||||
item => new VisualizationModel(item, { configModel })
|
||||
);
|
||||
}
|
||||
|
||||
const modalModel = ModalModel({
|
||||
destroy: () => this.$emit('close')
|
||||
});
|
||||
|
||||
const routerModel = {
|
||||
isDatasets: () => this.$props.dialogType === 'dataset',
|
||||
isMaps: () => this.$props.dialogType === 'maps'
|
||||
};
|
||||
|
||||
DialogView.setViewProperties({
|
||||
userModel,
|
||||
configModel,
|
||||
pollingModel: backgroundPollingModel,
|
||||
pollingView: this.$props.backgroundPollingView,
|
||||
routerModel: {
|
||||
model: routerModel
|
||||
}
|
||||
});
|
||||
|
||||
DialogView.addProperties({
|
||||
mamufasView: this.$props.mamufasImportView
|
||||
});
|
||||
|
||||
const DialogViewInstance = DialogView.openDialog(
|
||||
{ modalModel },
|
||||
{
|
||||
selectedItems,
|
||||
modalModel,
|
||||
viewElement: this.$refs.injectionHTMLElement,
|
||||
type: this.$props.dialogType
|
||||
}
|
||||
);
|
||||
|
||||
DialogViewInstance.render();
|
||||
|
||||
return DialogViewInstance;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.Dialog {
|
||||
.CreateDialog--new-dashboard {
|
||||
// We set content-box because it seems like
|
||||
// older content was laid out that way
|
||||
.Dialog-footer,
|
||||
.ImportOptions {
|
||||
box-sizing: content-box;
|
||||
|
||||
* {
|
||||
box-sizing: content-box;
|
||||
}
|
||||
}
|
||||
|
||||
// Reset style for radio button
|
||||
.RadioButton-input {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<div class="Dialog-content Dialog-content--expanded" ref="injectionHTMLElement"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DatasetMetadataDialog from 'builder/components/modals/dataset-metadata/dataset-metadata-view';
|
||||
import ModalModel from 'new-dashboard/plugins/backbone/modal-model';
|
||||
import VisualizationModel from 'dashboard/data/visualization-model';
|
||||
|
||||
export default {
|
||||
name: 'DatasetMetadata',
|
||||
props: {
|
||||
dataset: Object
|
||||
},
|
||||
mounted () {
|
||||
this.dialog = this.renderDialog();
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.dialog.clean();
|
||||
},
|
||||
methods: {
|
||||
renderDialog () {
|
||||
const modalModel = ModalModel({
|
||||
destroy: () => this.$emit('close')
|
||||
});
|
||||
|
||||
const visDefinitionModel = new VisualizationModel(
|
||||
this.$props.dataset,
|
||||
{ configModel: this.$cartoModels.config }
|
||||
);
|
||||
|
||||
visDefinitionModel.on('change', model => {
|
||||
this.$emit('updateVisualization', model);
|
||||
});
|
||||
|
||||
const datasetMetadataView = new DatasetMetadataDialog({
|
||||
modalModel,
|
||||
visDefinitionModel,
|
||||
configModel: this.$cartoModels.config,
|
||||
isLocked: visDefinitionModel.getTableModel().isSync(),
|
||||
el: this.$refs.injectionHTMLElement
|
||||
});
|
||||
|
||||
datasetMetadataView.render();
|
||||
|
||||
return datasetMetadataView;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<div class="Dialog-content Dialog-content--expanded" ref="injectionHTMLElement"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ModalModel from 'new-dashboard/plugins/backbone/modal-model';
|
||||
import DeleteAccount from 'dashboard/components/delete-account/delete-account-view';
|
||||
|
||||
export default {
|
||||
name: 'DeleteAccount',
|
||||
mounted () {
|
||||
this.dialog = this.renderDialog();
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.dialog.clean();
|
||||
},
|
||||
methods: {
|
||||
renderDialog () {
|
||||
const userModel = this.$cartoModels.user;
|
||||
const modalModel = ModalModel({
|
||||
destroy: () => {
|
||||
this.$emit('close');
|
||||
}
|
||||
});
|
||||
|
||||
const deleteAccountView = new DeleteAccount({
|
||||
modalModel,
|
||||
userModel,
|
||||
client: this.$store.state.client,
|
||||
el: this.$refs.injectionHTMLElement,
|
||||
clean_on_hide: true
|
||||
});
|
||||
|
||||
deleteAccountView.render();
|
||||
|
||||
return deleteAccountView;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<div class="Dialog-content Dialog-content--expanded">
|
||||
<div ref="injectionHTMLElement"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DeleteItemsView from 'dashboard/views/dashboard/dialogs/delete-items/delete-items-view';
|
||||
import DeleteItemsViewModel from 'dashboard/views/dashboard/dialogs/delete-items/delete-items-view-model';
|
||||
import VisualizationModel from 'dashboard/data/visualization-model';
|
||||
import ModalModel from 'new-dashboard/plugins/backbone/modal-model';
|
||||
|
||||
export default {
|
||||
name: 'DeleteDialog',
|
||||
props: {
|
||||
visualizations: Array,
|
||||
visualization: Object,
|
||||
contentType: String
|
||||
},
|
||||
mounted () {
|
||||
this.dialog = this.renderDialog();
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.dialog.clean();
|
||||
},
|
||||
methods: {
|
||||
renderDialog () {
|
||||
const configModel = this.$cartoModels.config;
|
||||
const modalModel = ModalModel({
|
||||
destroy: function () { this.$emit('close'); }.bind(this)
|
||||
});
|
||||
|
||||
const visualizationItems = this.$props.visualization
|
||||
? [new VisualizationModel(this.$props.visualization, { configModel })]
|
||||
: this.$props.visualizations.map(visualization => new VisualizationModel(visualization, { configModel }));
|
||||
|
||||
const viewModel = new DeleteItemsViewModel(visualizationItems, {
|
||||
contentType: this.$props.contentType
|
||||
});
|
||||
|
||||
viewModel.bind('DeleteItemsDone', () => {
|
||||
this.$store.dispatch('user/updateData'); // needed in order to keep the 'quota' synchronized
|
||||
this.$emit('fetchList');
|
||||
this.$emit('deselectAll');
|
||||
});
|
||||
|
||||
const deleteItemsView = new DeleteItemsView({
|
||||
modalModel,
|
||||
viewModel,
|
||||
configModel,
|
||||
userModel: this.$cartoModels.user,
|
||||
el: this.$refs.injectionHTMLElement
|
||||
});
|
||||
|
||||
deleteItemsView.render();
|
||||
|
||||
return deleteItemsView;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<div class="Dialog-content Dialog-content--expanded" ref="injectionHTMLElement"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DuplicateMap from 'dashboard/views/dashboard/dialogs/duplicate-vis/duplicate-vis-view';
|
||||
import VisualizationModel from 'dashboard/data/visualization-model';
|
||||
|
||||
export default {
|
||||
name: 'DuplicateMap',
|
||||
props: {
|
||||
visualization: Object
|
||||
},
|
||||
mounted () {
|
||||
this.dialog = this.renderDialog();
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.dialog.clean();
|
||||
},
|
||||
methods: {
|
||||
renderDialog () {
|
||||
const configModel = this.$cartoModels.config;
|
||||
const visDefinitionModel = new VisualizationModel(this.$props.visualization, { configModel });
|
||||
const table = visDefinitionModel.tableMetadata();
|
||||
|
||||
const duplicateMapView = new DuplicateMap({
|
||||
model: visDefinitionModel,
|
||||
table,
|
||||
configModel,
|
||||
userModel: this.$cartoModels.user,
|
||||
el: this.$refs.injectionHTMLElement,
|
||||
clean_on_hide: true
|
||||
});
|
||||
|
||||
duplicateMapView.render();
|
||||
|
||||
return duplicateMapView;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<div class="Dialog-content Dialog-content--expanded">
|
||||
<div ref="injectionHTMLElement"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ChangeLockView from 'dashboard/views/dashboard/dialogs/change-lock/change-lock-view';
|
||||
import ChangeLockViewModel from 'dashboard/views/dashboard/dialogs/change-lock/change-lock-view-model';
|
||||
import VisualizationModel from 'dashboard/data/visualization-model';
|
||||
import ModalModel from 'new-dashboard/plugins/backbone/modal-model';
|
||||
|
||||
export default {
|
||||
name: 'LockVisualization',
|
||||
props: {
|
||||
visualizations: Array,
|
||||
visualization: Object,
|
||||
contentType: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.dialog = this.renderDialog();
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.dialog.clean();
|
||||
},
|
||||
methods: {
|
||||
renderDialog () {
|
||||
const configModel = this.$cartoModels.config;
|
||||
|
||||
const modalModel = ModalModel({
|
||||
destroy: () => this.$emit('close')
|
||||
});
|
||||
|
||||
const visualizationItems = this.$props.visualization
|
||||
? [new VisualizationModel(this.$props.visualization, { configModel })]
|
||||
: this.$props.visualizations.map(visualization => new VisualizationModel(visualization, { configModel }));
|
||||
|
||||
const viewModel = new ChangeLockViewModel({
|
||||
items: visualizationItems,
|
||||
contentType: this.$props.contentType
|
||||
});
|
||||
|
||||
viewModel.bind('change:state', () => {
|
||||
if (viewModel.get('state') === 'ProcessItemsDone') {
|
||||
this.$emit('fetchList');
|
||||
this.$emit('deselectAll');
|
||||
this.$emit('close');
|
||||
}
|
||||
});
|
||||
|
||||
const changeLockView = new ChangeLockView({
|
||||
modalModel,
|
||||
model: viewModel,
|
||||
el: this.$refs.injectionHTMLElement
|
||||
});
|
||||
|
||||
changeLockView.render();
|
||||
|
||||
return changeLockView;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<div class="Dialog-content Dialog-content--expanded" ref="injectionHTMLElement"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MapMetadataDialog from 'builder/components/modals/map-metadata/map-metadata-view';
|
||||
import ModalModel from 'new-dashboard/plugins/backbone/modal-model';
|
||||
import VisualizationModel from 'dashboard/data/visualization-model';
|
||||
|
||||
export default {
|
||||
name: 'MapMetadata',
|
||||
props: {
|
||||
visualization: Object
|
||||
},
|
||||
mounted () {
|
||||
this.dialog = this.renderDialog();
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.dialog.clean();
|
||||
},
|
||||
methods: {
|
||||
renderDialog () {
|
||||
const modalModel = ModalModel({
|
||||
destroy: () => this.$emit('close')
|
||||
});
|
||||
|
||||
const visDefinitionModel = new VisualizationModel(
|
||||
this.$props.visualization,
|
||||
{ configModel: this.$cartoModels.config }
|
||||
);
|
||||
|
||||
visDefinitionModel.on('change', model => {
|
||||
this.$emit('updateVisualization', model);
|
||||
});
|
||||
|
||||
const mapMetadataView = new MapMetadataDialog({
|
||||
modalModel,
|
||||
visDefinitionModel,
|
||||
el: this.$refs.injectionHTMLElement
|
||||
});
|
||||
|
||||
mapMetadataView.render();
|
||||
|
||||
return mapMetadataView;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<div class="Dialog-content Dialog-content--expanded" ref="injectionHTMLElement"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ShareView from 'dashboard/views/dashboard/dialogs/share/share-view';
|
||||
import VisualizationModel from 'dashboard/data/visualization-model';
|
||||
import Dialog from 'new-dashboard/components/Backbone/Dialog';
|
||||
import ModalModel from 'new-dashboard/plugins/backbone/modal-model';
|
||||
|
||||
export default {
|
||||
name: 'Share',
|
||||
props: {
|
||||
visualization: Object
|
||||
},
|
||||
mounted () {
|
||||
this.dialog = this.renderDialog();
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.dialog.clean();
|
||||
},
|
||||
methods: {
|
||||
renderDialog () {
|
||||
const configModel = this.$cartoModels.config;
|
||||
const userModel = this.$cartoModels.user;
|
||||
|
||||
const modalsServiceModel = {
|
||||
create: createModalView => this.openDialog(createModalView)
|
||||
};
|
||||
|
||||
const modalModel = ModalModel({
|
||||
destroy: () => this.$emit('close')
|
||||
});
|
||||
|
||||
const visModel = new VisualizationModel(this.$props.visualization, { configModel });
|
||||
|
||||
const permissionModel = visModel.getPermissionModel();
|
||||
permissionModel.on('change', model => {
|
||||
this.$emit('updateVisualization', visModel);
|
||||
});
|
||||
|
||||
const viewModel = new ShareView({
|
||||
configModel,
|
||||
userModel,
|
||||
visModel,
|
||||
modals: modalsServiceModel,
|
||||
modalModel,
|
||||
onClose: () => {},
|
||||
el: this.$refs.injectionHTMLElement
|
||||
});
|
||||
|
||||
viewModel.render();
|
||||
|
||||
return viewModel;
|
||||
},
|
||||
|
||||
openDialog (createModalView) {
|
||||
const modalModel = {
|
||||
destroy: () => {}
|
||||
};
|
||||
|
||||
const modalView = createModalView(modalModel);
|
||||
modalView.render();
|
||||
|
||||
this.$modal.show({
|
||||
template: `
|
||||
<Dialog v-on:close="$emit('close')">
|
||||
<div ref="dialogInjectionNode"></div>
|
||||
</Dialog>`,
|
||||
components: { Dialog },
|
||||
mounted: function () {
|
||||
this.$refs.dialogInjectionNode.appendChild(modalView.el);
|
||||
|
||||
// Hack to be able to close Share Modal
|
||||
modalModel.destroy = () => {
|
||||
this.$emit('close');
|
||||
};
|
||||
}
|
||||
}, {}, { width: '100%', height: '100%' });
|
||||
|
||||
return modalModel;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<div ref="injectionHTMLElement"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MamufasImportView from 'dashboard/components/mamufas-import/mamufas-import-view';
|
||||
|
||||
export default {
|
||||
name: 'MamufasImportView',
|
||||
inject: ['backboneViews'],
|
||||
mounted () {
|
||||
this.mamufasView = this.initView();
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.mamufasView.clean();
|
||||
},
|
||||
methods: {
|
||||
initView () {
|
||||
const mamufasView = new MamufasImportView({
|
||||
el: document.body,
|
||||
userModel: this.$cartoModels.user
|
||||
});
|
||||
|
||||
mamufasView.on('dialogOpened', () => {
|
||||
this.$cartoModels.backgroundPolling.stopPollings();
|
||||
});
|
||||
|
||||
mamufasView.on('dialogClosed', () => {
|
||||
this.$cartoModels.backgroundPolling.startPollings();
|
||||
});
|
||||
|
||||
mamufasView.on('fileDropped', files => {
|
||||
const backgroundPollingView = this.backboneViews.backgroundPollingView.getBackgroundPollingView();
|
||||
const filesToUpload = files.length ? Array.from(files) : [files];
|
||||
|
||||
filesToUpload.forEach(file => {
|
||||
this.addVisPropertyToFile(file);
|
||||
backgroundPollingView._onDroppedFile(file);
|
||||
});
|
||||
});
|
||||
|
||||
mamufasView.enable();
|
||||
|
||||
return mamufasView;
|
||||
},
|
||||
|
||||
getView () {
|
||||
return this.mamufasView;
|
||||
},
|
||||
|
||||
addVisPropertyToFile (file) {
|
||||
if (this.isCARTOFile(file.name)) {
|
||||
file._createVis = true;
|
||||
}
|
||||
},
|
||||
|
||||
isCARTOFile (filename) {
|
||||
return filename.indexOf('.carto') > -1 ||
|
||||
filename.indexOf('.zip') > -1;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div class="BadgeWarning">
|
||||
<img class="BadgeWarning__icon" src="../assets/icons/common/warning-icon.svg" width="16" height="16" />
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'BadgeWarning'
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import 'new-dashboard/styles/variables';
|
||||
|
||||
.BadgeWarning {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0 1em;
|
||||
padding: .4em 1em;
|
||||
border-radius: 30px;
|
||||
background-color: rgba($warning__bg-color, 0.2);
|
||||
font-size: 12px;
|
||||
|
||||
&__icon {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<section class="bulk-actions">
|
||||
<template
|
||||
v-for="action in actions">
|
||||
<button
|
||||
class="bulk-actions__button button button--ghost"
|
||||
v-if="!action.shouldBeHidden"
|
||||
:class="{
|
||||
'is-txtAlert': action.isDestructive,
|
||||
'u-is-disabled': action.shouldBeDisabled}"
|
||||
:disabled="action.shouldBeDisabled"
|
||||
:key="action.event"
|
||||
@click="emitEvent(action.event)">
|
||||
{{ action.name }}
|
||||
</button>
|
||||
</template>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'BulkActions',
|
||||
props: {
|
||||
actions: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
emitEvent (eventName) {
|
||||
this.$emit(eventName);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.bulk-actions {
|
||||
display: flex;
|
||||
|
||||
&__button:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,228 @@
|
||||
<template>
|
||||
<BulkActions :actions="actions[actionMode]" v-on="getEventListeners()"></BulkActions>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import BulkActions from 'new-dashboard/components/BulkActions/BulkActions';
|
||||
import * as DialogActions from 'new-dashboard/core/dialog-actions';
|
||||
import * as Table from 'new-dashboard/core/models/table';
|
||||
import * as Visualization from 'new-dashboard/core/models/visualization';
|
||||
|
||||
export default {
|
||||
name: 'DatasetBulkActions',
|
||||
inject: ['backboneViews'],
|
||||
components: {
|
||||
BulkActions
|
||||
},
|
||||
props: {
|
||||
areAllDatasetsSelected: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
selectedDatasets: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
isOutOfDatasetsQuota: 'user/isOutOfDatasetsQuota'
|
||||
}),
|
||||
actions () {
|
||||
return {
|
||||
single: [
|
||||
{
|
||||
name: this.$t('BulkActions.datasets.selectAll'),
|
||||
event: 'selectAll',
|
||||
shouldBeHidden: this.areAllMapsSelected
|
||||
},
|
||||
{
|
||||
name: this.$t('BulkActions.datasets.createMap'),
|
||||
event: 'createMap',
|
||||
shouldBeHidden: this.isAnyLocked
|
||||
},
|
||||
{
|
||||
name: this.$t('BulkActions.datasets.changePrivacy'),
|
||||
event: 'changePrivacy',
|
||||
shouldBeHidden: this.isAnyShared || this.isAnyLocked
|
||||
},
|
||||
{
|
||||
name: this.$t('BulkActions.datasets.duplicate'),
|
||||
event: 'duplicateDataset',
|
||||
shouldBeDisabled: this.isOutOfDatasetsQuota
|
||||
},
|
||||
{
|
||||
name: this.$t('BulkActions.datasets.lock'),
|
||||
event: 'lockDataset',
|
||||
shouldBeDisabled: this.isAnyShared && !this.areAllLocked,
|
||||
shouldBeHidden: this.isAnyLocked
|
||||
},
|
||||
{
|
||||
name: this.$t('BulkActions.datasets.unlock'),
|
||||
event: 'unlockDataset',
|
||||
shouldBeDisabled: this.isAnyShared && this.areAllLocked,
|
||||
shouldBeHidden: !this.areAllLocked
|
||||
},
|
||||
{
|
||||
name: this.$t('BulkActions.datasets.delete'),
|
||||
event: 'deleteDataset',
|
||||
isDestructive: true,
|
||||
shouldBeDisabled: this.isAnyShared && !this.isAnyLocked,
|
||||
shouldBeHidden: this.isAnyLocked
|
||||
}
|
||||
],
|
||||
multiple: [
|
||||
{
|
||||
name: this.$t('BulkActions.datasets.selectAll'),
|
||||
event: 'selectAll',
|
||||
shouldBeHidden: this.areAllDatasetsSelected
|
||||
},
|
||||
{
|
||||
name: this.$t('BulkActions.datasets.deselectAll'),
|
||||
event: 'deselectAll'
|
||||
},
|
||||
{
|
||||
name: this.$t('BulkActions.datasets.createMap'),
|
||||
event: 'createMap',
|
||||
shouldBeHidden: this.isAnyLocked
|
||||
},
|
||||
{
|
||||
name: this.$t('BulkActions.datasets.lock'),
|
||||
event: 'lockDatasets',
|
||||
shouldBeDisabled: this.isAnyShared && !this.areAllLocked,
|
||||
shouldBeHidden: this.isAnyLocked
|
||||
},
|
||||
{
|
||||
name: this.$t('BulkActions.datasets.unlock'),
|
||||
event: 'unlockDatasets',
|
||||
shouldBeDisabled: this.isAnyShared && this.areAllLocked,
|
||||
shouldBeHidden: !this.areAllLocked
|
||||
},
|
||||
{
|
||||
name: this.$t('BulkActions.datasets.delete'),
|
||||
event: 'deleteDatasets',
|
||||
isDestructive: true,
|
||||
shouldBeDisabled: this.isAnyShared && !this.isAnyLocked,
|
||||
shouldBeHidden: this.isAnyLocked
|
||||
}
|
||||
]
|
||||
};
|
||||
},
|
||||
actionMode () {
|
||||
return this.selectedDatasets.length > 1 ? 'multiple' : 'single';
|
||||
},
|
||||
isAnyShared () {
|
||||
return this.selectedDatasets.some(dataset => Visualization.isSharedWithMe(dataset, this.$cartoModels));
|
||||
},
|
||||
isAnyLocked () {
|
||||
return this.selectedDatasets.some(dataset => dataset.locked);
|
||||
},
|
||||
areAllLocked () {
|
||||
return this.selectedDatasets.every(dataset => dataset.locked);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getActionHandlers () {
|
||||
return {
|
||||
deselectAll: () => {
|
||||
this.deselectAll();
|
||||
},
|
||||
fetchList: () => {
|
||||
this.$store.dispatch('datasets/fetch');
|
||||
},
|
||||
updateVisualization: (model) => {
|
||||
this.$store.dispatch('datasets/updateVisualization', { visualizationId: model.get('id'), visualizationAttributes: model.attributes });
|
||||
}
|
||||
};
|
||||
},
|
||||
getEventListeners () {
|
||||
const events = this.actions[this.actionMode].map(action => action.event);
|
||||
|
||||
return events.reduce(
|
||||
(eventListeners, action) => {
|
||||
eventListeners[action] = this[action].bind(this);
|
||||
return eventListeners;
|
||||
}, {}
|
||||
);
|
||||
},
|
||||
showModal (componentDefinition, componentPropsData) {
|
||||
this.$modal.show(
|
||||
componentDefinition,
|
||||
componentPropsData,
|
||||
{ width: '100%', height: '100%' }
|
||||
);
|
||||
},
|
||||
selectAll () {
|
||||
this.$emit('selectAll');
|
||||
},
|
||||
deselectAll () {
|
||||
this.$emit('deselectAll');
|
||||
},
|
||||
createMap () {
|
||||
DialogActions.createMap.apply(this, [
|
||||
this.selectedDatasets,
|
||||
this.backboneViews.backgroundPollingView.getBackgroundPollingView(),
|
||||
this.backboneViews.mamufasImportView.getView()
|
||||
]);
|
||||
},
|
||||
changePrivacy () {
|
||||
DialogActions.changePrivacy.apply(this, [this.selectedDatasets[0], this.getActionHandlers()]);
|
||||
},
|
||||
duplicateDataset () {
|
||||
const selectedDataset = this.selectedDatasets[0];
|
||||
const bgPollingView = this.backboneViews.backgroundPollingView.getBackgroundPollingView();
|
||||
|
||||
bgPollingView._addDataset({
|
||||
type: 'duplication',
|
||||
table_name: `${Table.getUnqualifiedName(selectedDataset.name)}_copy`,
|
||||
value: selectedDataset.table.name,
|
||||
create_vis: false
|
||||
});
|
||||
this.deselectAll();
|
||||
},
|
||||
unlockDataset () {
|
||||
DialogActions.changeLockState.apply(this, [
|
||||
this.selectedDatasets[0],
|
||||
'datasets',
|
||||
this.getActionHandlers()
|
||||
]);
|
||||
},
|
||||
lockDataset () {
|
||||
DialogActions.changeLockState.apply(this, [
|
||||
this.selectedDatasets[0],
|
||||
'datasets',
|
||||
this.getActionHandlers()
|
||||
]);
|
||||
},
|
||||
deleteDataset () {
|
||||
DialogActions.deleteVisualization.apply(this, [
|
||||
this.selectedDatasets[0],
|
||||
'datasets',
|
||||
this.getActionHandlers()
|
||||
]);
|
||||
},
|
||||
unlockDatasets () {
|
||||
DialogActions.changeVisualizationsLockState.apply(this, [
|
||||
this.selectedDatasets,
|
||||
'datasets',
|
||||
this.getActionHandlers()
|
||||
]);
|
||||
},
|
||||
lockDatasets () {
|
||||
DialogActions.changeVisualizationsLockState.apply(this, [
|
||||
this.selectedDatasets,
|
||||
'datasets',
|
||||
this.getActionHandlers()
|
||||
]);
|
||||
},
|
||||
deleteDatasets () {
|
||||
DialogActions.deleteVisualizations.apply(this, [
|
||||
this.selectedDatasets,
|
||||
'datasets',
|
||||
this.getActionHandlers()
|
||||
]);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,210 @@
|
||||
<template>
|
||||
<BulkActions :actions="actions[actionMode]" v-on="getEventListeners()"></BulkActions>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import BulkActions from 'new-dashboard/components/BulkActions/BulkActions';
|
||||
import * as DialogActions from 'new-dashboard/core/dialog-actions';
|
||||
import * as Visualization from 'new-dashboard/core/models/visualization';
|
||||
|
||||
export default {
|
||||
name: 'MapBulkActions',
|
||||
components: {
|
||||
BulkActions
|
||||
},
|
||||
props: {
|
||||
areAllMapsSelected: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
selectedMaps: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
isOutOfPublicMapsQuota: 'user/isOutOfPublicMapsQuota'
|
||||
}),
|
||||
actions () {
|
||||
return {
|
||||
single: [
|
||||
{
|
||||
name: this.$t('BulkActions.maps.selectAll'),
|
||||
event: 'selectAll',
|
||||
shouldBeHidden: this.areAllMapsSelected
|
||||
},
|
||||
{
|
||||
name: this.$t('BulkActions.maps.changePrivacy'),
|
||||
event: 'changePrivacy',
|
||||
shouldBeDisabled: !this.canChangePrivacy,
|
||||
shouldBeHidden: this.isAnyShared || this.isAnyLocked
|
||||
},
|
||||
{
|
||||
name: this.$t('BulkActions.maps.duplicate'),
|
||||
event: 'duplicateMap',
|
||||
shouldBeDisabled: !this.canDuplicate
|
||||
},
|
||||
{
|
||||
name: this.$t('BulkActions.maps.lock'),
|
||||
event: 'lockMap',
|
||||
shouldBeDisabled: this.isAnyShared && !this.areAllLocked,
|
||||
shouldBeHidden: this.isAnyLocked
|
||||
},
|
||||
{
|
||||
name: this.$t('BulkActions.maps.unlock'),
|
||||
event: 'unlockMap',
|
||||
shouldBeDisabled: this.isAnyShared && this.areAllLocked,
|
||||
shouldBeHidden: !this.areAllLocked
|
||||
},
|
||||
{
|
||||
name: this.$t('BulkActions.maps.delete'),
|
||||
event: 'deleteMap',
|
||||
isDestructive: true,
|
||||
shouldBeDisabled: this.isAnyShared && !this.isAnyLocked,
|
||||
shouldBeHidden: this.isAnyLocked
|
||||
}
|
||||
],
|
||||
multiple: [
|
||||
{
|
||||
name: this.$t('BulkActions.maps.selectAll'),
|
||||
event: 'selectAll',
|
||||
shouldBeHidden: this.areAllMapsSelected
|
||||
},
|
||||
{
|
||||
name: this.$t('BulkActions.maps.deselectAll'),
|
||||
event: 'deselectAll'
|
||||
},
|
||||
{
|
||||
name: this.$t('BulkActions.maps.lock'),
|
||||
event: 'lockMaps',
|
||||
shouldBeDisabled: this.isAnyShared && !this.areAllLocked,
|
||||
shouldBeHidden: this.isAnyLocked
|
||||
},
|
||||
{
|
||||
name: this.$t('BulkActions.maps.unlock'),
|
||||
event: 'unlockMaps',
|
||||
shouldBeDisabled: this.isAnyShared && this.areAllLocked,
|
||||
shouldBeHidden: !this.areAllLocked
|
||||
},
|
||||
{
|
||||
name: this.$t('BulkActions.maps.delete'),
|
||||
event: 'deleteMaps',
|
||||
isDestructive: true,
|
||||
shouldBeDisabled: this.isAnyShared && !this.isAnyLocked,
|
||||
shouldBeHidden: this.isAnyLocked
|
||||
}
|
||||
]
|
||||
};
|
||||
},
|
||||
actionMode () {
|
||||
return this.selectedMaps.length > 1 ? 'multiple' : 'single';
|
||||
},
|
||||
isAnyShared () {
|
||||
return this.selectedMaps.some(map => Visualization.isSharedWithMe(map, this.$cartoModels));
|
||||
},
|
||||
isAnyLocked () {
|
||||
return this.selectedMaps.some(map => map.locked);
|
||||
},
|
||||
areAllLocked () {
|
||||
return this.selectedMaps.every(map => map.locked);
|
||||
},
|
||||
isSelectedMapPrivate () {
|
||||
return this.selectedMaps.some(map => ['PRIVATE'].includes(map.privacy));
|
||||
},
|
||||
canChangePrivacy () {
|
||||
return !this.isOutOfPublicMapsQuota || !this.isSelectedMapPrivate;
|
||||
},
|
||||
canDuplicate () {
|
||||
return !this.isOutOfPublicMapsQuota || this.isSelectedMapPrivate;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getActionHandlers () {
|
||||
return {
|
||||
deselectAll: () => {
|
||||
this.deselectAll();
|
||||
},
|
||||
fetchList: () => {
|
||||
this.$store.dispatch('maps/fetch');
|
||||
},
|
||||
updateVisualization: (model) => {
|
||||
this.$store.dispatch('maps/updateVisualization', { visualizationId: model.get('id'), visualizationAttributes: model.attributes });
|
||||
}
|
||||
};
|
||||
},
|
||||
getEventListeners () {
|
||||
const events = this.actions[this.actionMode].map(action => action.event);
|
||||
|
||||
return events.reduce(
|
||||
(eventListeners, action) => {
|
||||
eventListeners[action] = this[action].bind(this);
|
||||
return eventListeners;
|
||||
}, {}
|
||||
);
|
||||
},
|
||||
showModal (componentDefinition, componentPropsData) {
|
||||
this.$modal.show(
|
||||
componentDefinition,
|
||||
componentPropsData,
|
||||
{ width: '100%', height: '100%' }
|
||||
);
|
||||
},
|
||||
selectAll () {
|
||||
this.$emit('selectAll');
|
||||
},
|
||||
deselectAll () {
|
||||
this.$emit('deselectAll');
|
||||
},
|
||||
changePrivacy () {
|
||||
DialogActions.changePrivacy.apply(this, [this.selectedMaps[0], this.getActionHandlers()]);
|
||||
},
|
||||
duplicateMap () {
|
||||
DialogActions.duplicateVisualization.apply(this, [this.selectedMaps[0]]);
|
||||
},
|
||||
unlockMap () {
|
||||
DialogActions.changeLockState.apply(this, [
|
||||
this.selectedMaps[0],
|
||||
'maps',
|
||||
this.getActionHandlers()
|
||||
]);
|
||||
},
|
||||
lockMap () {
|
||||
DialogActions.changeLockState.apply(this, [
|
||||
this.selectedMaps[0],
|
||||
'maps',
|
||||
this.getActionHandlers()
|
||||
]);
|
||||
},
|
||||
deleteMap () {
|
||||
DialogActions.deleteVisualization.apply(this, [
|
||||
this.selectedMaps[0],
|
||||
'maps',
|
||||
this.getActionHandlers()
|
||||
]);
|
||||
},
|
||||
unlockMaps () {
|
||||
DialogActions.changeVisualizationsLockState.apply(this, [
|
||||
this.selectedMaps,
|
||||
'maps',
|
||||
this.getActionHandlers()
|
||||
]);
|
||||
},
|
||||
lockMaps () {
|
||||
DialogActions.changeVisualizationsLockState.apply(this, [
|
||||
this.selectedMaps,
|
||||
'maps',
|
||||
this.getActionHandlers()
|
||||
]);
|
||||
},
|
||||
deleteMaps () {
|
||||
DialogActions.deleteVisualizations.apply(this, [
|
||||
this.selectedMaps,
|
||||
'maps',
|
||||
this.getActionHandlers()
|
||||
]);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,142 @@
|
||||
<template>
|
||||
<a class="catalogCard" @click="onClick">
|
||||
|
||||
<div class="catalogCard__column">
|
||||
<div class="catalogCard__cell cell">
|
||||
<div class="catalogCard__cell--category">
|
||||
<div class="catalog__icon" :class="`catalog__icon--${formattedCategory}`"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cell cell--main u-flex u-flex__align--center">
|
||||
<span class="text is-caption is-txtGrey u-ellipsis catalogCard__title">
|
||||
{{ dataset.name }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="catalogCard__column">
|
||||
<div class="cell catalogCard__cell--large u-ellipsis">
|
||||
<span class="text is-small is-txtSoftGrey">
|
||||
{{ formattedSpatialAggregations}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="catalogCard__column">
|
||||
<div class="cell cell--large u-ellipsis">
|
||||
<span class="text is-small is-txtSoftGrey">
|
||||
{{ formattedFrequency }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="cell cell--large u-ellipsis">
|
||||
<span class="text is-small is-txtSoftGrey">
|
||||
{{ formattedSource }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: 'CatalogCard',
|
||||
props: {
|
||||
dataset: Object
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
categories: state => state.catalog.categories
|
||||
}),
|
||||
formattedSpatialAggregations () {
|
||||
return this.dataset.spatial_aggregations.join(', ');
|
||||
},
|
||||
formattedFrequency () {
|
||||
return this.dataset.frequency.join(', ');
|
||||
},
|
||||
formattedSource () {
|
||||
return this.dataset.source.join(', ');
|
||||
},
|
||||
formattedCategory () {
|
||||
if (!this.categories.includes(this.dataset.category)) {
|
||||
return 'default';
|
||||
}
|
||||
return this.dataset.category.replace(/ /g, '-').toLowerCase();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onClick (event) {
|
||||
event.preventDefault();
|
||||
this.$router.push({
|
||||
name: 'catalog_detail',
|
||||
params: {
|
||||
id: this.dataset.id
|
||||
},
|
||||
query: {
|
||||
category: this.dataset.category,
|
||||
country: this.dataset.country
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import 'new-dashboard/styles/variables';
|
||||
|
||||
.catalogCard {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
overflow: hidden;
|
||||
border-bottom: 1px solid $softblue;
|
||||
background-color: $white;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: $softblue;
|
||||
text-decoration: none;
|
||||
|
||||
.catalogCard__title {
|
||||
color: $primary-color;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
&__column {
|
||||
display: flex;
|
||||
flex: 0 0 33.3334%;
|
||||
justify-content: center;
|
||||
max-width: 33.3334%;
|
||||
}
|
||||
|
||||
&__cell {
|
||||
width: 58px;
|
||||
height: 100%;
|
||||
padding-left: 0;
|
||||
|
||||
&--large {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
width: 280px;
|
||||
}
|
||||
|
||||
&--category {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
margin-right: 6px;
|
||||
padding: 6px;
|
||||
overflow: hidden;
|
||||
transition: all 0.25s cubic-bezier(0.4, 0.01, 0.165, 0.99);
|
||||
border-radius: 2px;
|
||||
background-color: $thumbnail__bg-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,345 @@
|
||||
<template>
|
||||
<div
|
||||
v-click-outside="closeDropdown"
|
||||
class="catalogDropdown"
|
||||
@keydown.down.prevent="onKeydownDown"
|
||||
@keydown.up.prevent="onKeydownUp"
|
||||
:class="{'is-disabled': isDisabled, 'is-open': isOpen}">
|
||||
<span class="title is-caption catalogDropdown__label">{{ title }}</span>
|
||||
<div class="catalogDropdown__container">
|
||||
<div v-if="showInput">
|
||||
<input type="text"
|
||||
class="text is-caption catalogDropdown__input"
|
||||
:class="{ 'has-error': hasError }"
|
||||
:placeholder="[isOpen ? $t('CatalogDropdown.placeholder') : placeholder]"
|
||||
v-model="searchFilter"
|
||||
@focus="onInputFocus"
|
||||
@click="openDropdown"
|
||||
@keyup.enter="onKeyEnter"
|
||||
:disabled="isDisabled || hasError">
|
||||
</div>
|
||||
<div v-else
|
||||
class="text is-caption catalogDropdown__input"
|
||||
:class="{ 'has-error': hasError }"
|
||||
@click="openDropdown">
|
||||
<CatalogDropdownItem :option="searchFilter"/>
|
||||
<button class="catalogDropdown__close" @click="reset"><img src="../../assets/icons/common/dropdown-close.svg" width="16" height="20" /></button>
|
||||
</div>
|
||||
<p class="catalogDropdown__error text is-small" v-if="hasError">{{ error }}</p>
|
||||
<ul class="catalogDropdown__list"
|
||||
:class="{'is-open': isOpen, 'is-height-limited': limitHeight}"
|
||||
@mouseleave="resetActiveOption">
|
||||
<li
|
||||
v-for="(option, index) in filteredOptions" :key="option"
|
||||
@click="selectOption(option)"
|
||||
class="catalogDropdown__option text is-caption"
|
||||
:class="{'catalogDropdown__option--active': activeOptionIndex === index + 1}"
|
||||
@mouseover="updateActiveOption(index + 1)">
|
||||
<CatalogDropdownItem :option="option"/>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="catalogDropdown__extra">
|
||||
<slot name="extra"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CatalogDropdownItem from './CatalogDropdownItem';
|
||||
|
||||
export default {
|
||||
name: 'CatalogDropdown',
|
||||
components: {
|
||||
CatalogDropdownItem
|
||||
},
|
||||
props: {
|
||||
title: String,
|
||||
placeholder: String,
|
||||
options: {
|
||||
type: Array,
|
||||
default () {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
open: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
limitHeight: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
selected: {},
|
||||
isInputFocused: false,
|
||||
activeOptionIndex: -1,
|
||||
searchFilter: '',
|
||||
isOpen: this.open,
|
||||
isDisabled: this.disabled,
|
||||
error: ''
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
filteredOptions () {
|
||||
const filtered = [];
|
||||
const regOption = new RegExp(this.searchFilter, 'ig');
|
||||
for (const option of this.options) {
|
||||
if (this.searchFilter.length < 1 || option.match(regOption)) {
|
||||
filtered.push(option);
|
||||
}
|
||||
}
|
||||
return filtered.length > 0 ? filtered : this.options;
|
||||
},
|
||||
hasError () {
|
||||
return this.error.length > 0;
|
||||
},
|
||||
showInput () {
|
||||
return Object.keys(this.selected).length === 0;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openDropdown () {
|
||||
this.isOpen = true;
|
||||
},
|
||||
closeDropdown () {
|
||||
this.isOpen = false;
|
||||
},
|
||||
disableDropdown () {
|
||||
this.isDisabled = true;
|
||||
},
|
||||
enableDropdown () {
|
||||
this.isDisabled = false;
|
||||
},
|
||||
onInputFocus () {
|
||||
this.isInputFocused = true;
|
||||
},
|
||||
onKeydownDown () {
|
||||
if (this.activeOptionIndex < this.options.length) {
|
||||
this.activeOptionIndex++;
|
||||
}
|
||||
},
|
||||
onKeydownUp () {
|
||||
if (this.activeOptionIndex > 0) {
|
||||
this.activeOptionIndex--;
|
||||
}
|
||||
},
|
||||
onKeyEnter () {
|
||||
const optionSelected = this.$el.querySelector('.catalogDropdown__option--active');
|
||||
if (optionSelected) {
|
||||
optionSelected.click();
|
||||
}
|
||||
},
|
||||
updateActiveOption (index) {
|
||||
this.activeOptionIndex = index;
|
||||
},
|
||||
resetActiveOption () {
|
||||
this.activeOptionIndex = -1;
|
||||
},
|
||||
selectOption (option) {
|
||||
this.setInput(option);
|
||||
this.$emit('selected', this.selected);
|
||||
},
|
||||
setInput (option) {
|
||||
this.enableDropdown();
|
||||
this.selected = option;
|
||||
this.searchFilter = this.selected;
|
||||
this.closeDropdown();
|
||||
},
|
||||
clearInput () {
|
||||
this.searchFilter = '';
|
||||
this.selected = {};
|
||||
this.closeDropdown();
|
||||
},
|
||||
reset () {
|
||||
this.clearInput();
|
||||
this.error = '';
|
||||
this.$emit('reset');
|
||||
this.openDropdown();
|
||||
},
|
||||
setError (error) {
|
||||
this.error = error;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
disabled () {
|
||||
this.isDisabled = this.disabled;
|
||||
},
|
||||
open () {
|
||||
this.isOpen = this.open;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import 'new-dashboard/styles/variables';
|
||||
|
||||
.catalogDropdown {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
|
||||
&__label {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
&__container {
|
||||
position: absolute;
|
||||
top: 48px;
|
||||
width: calc(100% - 20px);
|
||||
}
|
||||
|
||||
&__input {
|
||||
width: 100%;
|
||||
height: 56px;
|
||||
padding: 16px 24px;
|
||||
border: 1px solid $neutral--300;
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
|
||||
.catalogDropdown__close {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&.has-error {
|
||||
border: 1px solid $warning__border-color;
|
||||
}
|
||||
}
|
||||
|
||||
&__error {
|
||||
position: relative;
|
||||
margin-top: 4px;
|
||||
padding-left: 24px;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
left: 0;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background-image: url(../../assets/icons/common/warning-icon.svg);
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
&__close {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 18px;
|
||||
right: 24px;
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
opacity: 0.24;
|
||||
|
||||
.catalogDropdown__close {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-open {
|
||||
.catalogDropdown__input {
|
||||
border-bottom-right-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
border-top-color: transparent;
|
||||
border-right-color: transparent;
|
||||
border-left-color: transparent;
|
||||
}
|
||||
|
||||
.catalogDropdown__list {
|
||||
border: transparent;
|
||||
}
|
||||
|
||||
.catalogDropdown__container {
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 0 0 2px $primary-color;
|
||||
}
|
||||
|
||||
.catalogDropdown__extra {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.is-open) {
|
||||
.catalogDropdown__input:hover {
|
||||
border-color: $text__color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.catalogDropdown__list {
|
||||
visibility: hidden;
|
||||
border: 1px solid $softblue;
|
||||
opacity: 0;
|
||||
background-color: $white;
|
||||
pointer-events: none;
|
||||
|
||||
&.is-open {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
&.is-height-limited {
|
||||
max-height: 196px;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
}
|
||||
|
||||
.catalogDropdown__option {
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding: 12px 16px 12px 24px;
|
||||
overflow: hidden;
|
||||
color: $primary-color;
|
||||
text-decoration: none;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px solid $softblue;
|
||||
}
|
||||
|
||||
&--active {
|
||||
background-color: rgba($primary-color, 0.05);
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.catalogDropdown__extra {
|
||||
visibility: hidden;
|
||||
padding: 16px 24px;
|
||||
border-top: 1px solid $softblue;
|
||||
border-bottom-right-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
opacity: 0;
|
||||
background-color: $white;
|
||||
pointer-events: none;
|
||||
|
||||
&--text {
|
||||
color: $neutral--600;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div class="u-flex">
|
||||
<div :class="`catalog__icon catalog__icon--${getCSSModifier(option)}`"></div>
|
||||
<span>{{ option }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import getCSSModifier from 'new-dashboard/utils/get-css-modifier';
|
||||
|
||||
export default {
|
||||
name: 'CatalogDropdownItem',
|
||||
props: {
|
||||
option: String
|
||||
},
|
||||
methods: {
|
||||
getCSSModifier
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
.catalog__icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-right: 12px;
|
||||
background-size: contain;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<div class="catalogCardFake">
|
||||
|
||||
<div class="catalogCardFake__column u-flex u-flex__align--center">
|
||||
<div class="catalogCardFake__cell cell-placeholder cell thumbnail">
|
||||
<div class="icon-placeholder"/>
|
||||
</div>
|
||||
<div class="cell-placeholder cell cell--main">
|
||||
<div class="text-placeholder"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="catalogCardFake__column">
|
||||
<div class="cell-placeholder cell cell--xlarge">
|
||||
<div class="text-placeholder"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="catalogCardFake__column">
|
||||
<div class="cell-placeholder cell cell--large">
|
||||
<div class="text-placeholder"/>
|
||||
</div>
|
||||
<div class="cell-placeholder cell cell--large">
|
||||
<div class="text-placeholder"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'CatalogCardFake'
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import 'new-dashboard/styles/variables';
|
||||
|
||||
.catalogCardFake {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 81px;
|
||||
padding: 22px 14px;
|
||||
background-color: $white;
|
||||
|
||||
&__column {
|
||||
display: flex;
|
||||
flex: 0 0 33.3334%;
|
||||
justify-content: center;
|
||||
max-width: 33.3334%;
|
||||
}
|
||||
|
||||
&__cell {
|
||||
width: 58px;
|
||||
height: 100%;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.icon-placeholder {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 2px;
|
||||
background-color: $softblue;
|
||||
}
|
||||
|
||||
.text-placeholder {
|
||||
height: 24px;
|
||||
background-color: $softblue;
|
||||
}
|
||||
|
||||
.cell-placeholder {
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
padding-right: 16px;
|
||||
padding-left: 6px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,132 @@
|
||||
<template>
|
||||
<div class="catalogListHeader">
|
||||
<div class="catalogListHeader__column">
|
||||
<div class="catalogListHeader__cell cell">
|
||||
</div>
|
||||
<div class="cell cell--main">
|
||||
<span class="text is-small is-txtSoftGrey catalogListHeader__sort"
|
||||
:class="{
|
||||
'is-active': isOrderApplied('name'),
|
||||
'is-reversed': isReverseOrderApplied('name'),
|
||||
}">
|
||||
{{ $t('CatalogListHeader.name') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="catalogListHeader__column">
|
||||
<div class="cell catalogCard__cell--large">
|
||||
<span class="text is-small is-txtSoftGrey catalogListHeader__sort"
|
||||
:class="{
|
||||
'is-active': isOrderApplied('spatial_aggregations'),
|
||||
'is-reversed': isReverseOrderApplied('spatial_aggregations'),
|
||||
}">
|
||||
{{ $t('CatalogListHeader.aggregation') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="catalogListHeader__column">
|
||||
<div class="cell cell--large">
|
||||
<span class="text is-small is-txtSoftGrey catalogListHeader__sort"
|
||||
:class="{
|
||||
'is-active': isOrderApplied('frequency'),
|
||||
'is-reversed': isReverseOrderApplied('frequency'),
|
||||
}">
|
||||
{{ $t('CatalogListHeader.frequency') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="cell cell--large">
|
||||
<span class="text is-small is-txtSoftGrey catalogListHeader__sort"
|
||||
:class="{
|
||||
'is-active': isOrderApplied('source'),
|
||||
'is-reversed': isReverseOrderApplied('source'),
|
||||
}">
|
||||
{{ $t('CatalogListHeader.source') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'CatalogListHeader',
|
||||
props: {
|
||||
order: String,
|
||||
orderDirection: String
|
||||
},
|
||||
methods: {
|
||||
changeOrder (order) {
|
||||
if (this.order === order) {
|
||||
return this.setOrder(
|
||||
order,
|
||||
this.getOppositeOrderDirection(this.orderDirection)
|
||||
);
|
||||
}
|
||||
|
||||
this.setOrder(order);
|
||||
},
|
||||
getOppositeOrderDirection (order) {
|
||||
if (order === 'desc') {
|
||||
return 'asc';
|
||||
}
|
||||
|
||||
if (order === 'asc') {
|
||||
return 'desc';
|
||||
}
|
||||
},
|
||||
isOrderApplied (order) {
|
||||
return order === this.order;
|
||||
},
|
||||
|
||||
isReverseOrderApplied (order) {
|
||||
return order === this.order && this.orderDirection === 'asc';
|
||||
},
|
||||
setOrder (order, direction = 'desc') {
|
||||
this.$emit('changeOrder', { order, direction });
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import 'new-dashboard/styles/variables';
|
||||
|
||||
.catalogListHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
overflow: hidden;
|
||||
border-bottom: 1px solid $softblue;
|
||||
background-color: $white;
|
||||
cursor: default;
|
||||
|
||||
&__column {
|
||||
display: flex;
|
||||
flex: 0 0 33.3334%;
|
||||
max-width: 33.3334%;
|
||||
}
|
||||
|
||||
&__cell {
|
||||
width: 58px;
|
||||
height: 100%;
|
||||
padding-left: 0;
|
||||
|
||||
&--large {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
width: 280px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.catalogListHeader__sort {
|
||||
&.is-active,
|
||||
&:hover {
|
||||
color: $text__color;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<div class="catalogRequestSuccess">
|
||||
<div class="catalogRequestSuccess__icon">
|
||||
<img svg-inline src="../../assets/icons/catalog/request-success.svg" />
|
||||
</div>
|
||||
<p ref="catalogRequestSuccessText" class="catalogRequestSuccess__text text is-small">{{ $t('CatalogDetailPage.requestSuccess')}}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'CatalogRequestSuccess',
|
||||
mounted () {
|
||||
this.$refs.catalogRequestSuccessText.classList.add('animate');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "new-dashboard/styles/variables";
|
||||
|
||||
.catalogRequestSuccess {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 290px;
|
||||
|
||||
&__icon {
|
||||
flex-shrink: 0;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
margin-top: -4px;
|
||||
|
||||
.circle-path {
|
||||
animation: dash 0.5s linear forwards;
|
||||
animation-delay: 0s;
|
||||
stroke-dasharray: 80;
|
||||
stroke-dashoffset: 80;
|
||||
}
|
||||
|
||||
.check-path {
|
||||
animation: dash 0.5s linear forwards;
|
||||
animation-delay: 0s;
|
||||
stroke-dasharray: 35;
|
||||
stroke-dashoffset: 35;
|
||||
}
|
||||
}
|
||||
|
||||
&__text {
|
||||
margin-left: 24px;
|
||||
transition: opacity 1s ease 0.5s;
|
||||
opacity: 0;
|
||||
|
||||
&.animate {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dash {
|
||||
to {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<textarea ref="code" v-model="code"></textarea>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CodeMirror from 'codemirror';
|
||||
import 'codemirror/mode/javascript/javascript';
|
||||
import 'codemirror/mode/python/python';
|
||||
import 'codemirror/mode/shell/shell';
|
||||
import 'codemirror/mode/htmlmixed/htmlmixed';
|
||||
import 'codemirror/lib/codemirror.css';
|
||||
import 'codemirror/theme/material.css';
|
||||
|
||||
export default {
|
||||
name: 'CodeBlock',
|
||||
data () {
|
||||
return {
|
||||
codemirror: null
|
||||
};
|
||||
},
|
||||
mounted () {
|
||||
this.initialize();
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.destroy();
|
||||
},
|
||||
props: {
|
||||
code: String,
|
||||
language: {
|
||||
type: String,
|
||||
default: 'javascript'
|
||||
},
|
||||
lineNumbers: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
theme: {
|
||||
type: String,
|
||||
default: 'material'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
initialize () {
|
||||
const allOptions = {
|
||||
mode: this.language,
|
||||
lineNumbers: this.lineNumbers,
|
||||
theme: this.theme,
|
||||
...defaultOptions
|
||||
};
|
||||
this.codemirror = CodeMirror.fromTextArea(this.$refs.code, allOptions);
|
||||
},
|
||||
destroy () {
|
||||
const codemirrorElement = this.codemirror.doc.cm.getWrapperElement();
|
||||
codemirrorElement.remove && codemirrorElement.remove();
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
code () {
|
||||
this.codemirror && this.codemirror.setValue(this.code);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const defaultOptions = {
|
||||
readOnly: true,
|
||||
addModeClass: true
|
||||
};
|
||||
|
||||
</script>
|
||||
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<div class="jupyter">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'JupyterBlock'
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import 'new-dashboard/styles/variables';
|
||||
|
||||
.jupyter {
|
||||
margin: 16px 0;
|
||||
padding: 20px 20px 20px 8px;
|
||||
background-color: $white;
|
||||
}
|
||||
|
||||
.jupyter > * {
|
||||
margin-bottom: 16px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<div :class="inputClass" class="u-flex u-flex__align--start">
|
||||
<span class="jupyter-title u-mr--8">{{ $t(`Wizards.cartoframes.jupyter.${inputType}`) }}</span>
|
||||
<div class="jupyter-content">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'JupyterBlock',
|
||||
props: {
|
||||
isInput: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
inputType () {
|
||||
return this.isInput ? 'in' : 'out';
|
||||
},
|
||||
inputClass () {
|
||||
return `jupyter-${this.inputType}`;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style lang="scss">
|
||||
@import 'new-dashboard/styles/variables';
|
||||
|
||||
.jupyter-title {
|
||||
width: 35px;
|
||||
margin-top: 11px;
|
||||
font-family: 'Source Code Pro', monospace;
|
||||
font-size: 14px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.jupyter-content {
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
|
||||
.CodeMirror {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.jupyter-in {
|
||||
.jupyter-title {
|
||||
color: $jupyter__text--in;
|
||||
}
|
||||
|
||||
.CodeMirror {
|
||||
border: 1px solid $jupyter__border-color;
|
||||
background-color: $jupyter__fill-color;
|
||||
}
|
||||
}
|
||||
|
||||
.jupyter-out {
|
||||
.jupyter-title {
|
||||
color: $jupyter__text--out;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<button class="button is-primary" @click="openCreateModal" :class="{'u-is-disabled': disabled}">
|
||||
<slot />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Dialog from 'new-dashboard/components/Backbone/Dialog.vue';
|
||||
import CreateDialog from 'new-dashboard/components/Backbone/Dialogs/CreateDialog.vue';
|
||||
|
||||
export default {
|
||||
name: 'CreateButton',
|
||||
inject: ['backboneViews'],
|
||||
props: {
|
||||
visualizationType: {
|
||||
type: String,
|
||||
default: 'maps'
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openCreateModal () {
|
||||
this.$modal.show({
|
||||
template: `
|
||||
<Dialog @close="$emit('close')">
|
||||
<CreateDialog
|
||||
:dialogType="dialogType"
|
||||
:backgroundPollingView="backgroundPollingView"
|
||||
:mamufasImportView="mamufasImportView"
|
||||
@close="$emit('close')" />
|
||||
</Dialog>`,
|
||||
props: ['dialogType', 'backgroundPollingView', 'mamufasImportView'],
|
||||
components: { Dialog, CreateDialog }
|
||||
},
|
||||
{
|
||||
dialogType: this.$props.visualizationType,
|
||||
backgroundPollingView: this.backboneViews.backgroundPollingView.getBackgroundPollingView(),
|
||||
mamufasImportView: this.backboneViews.mamufasImportView.getView()
|
||||
},
|
||||
{
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<InitialState :title="headerText">
|
||||
<template slot="icon">
|
||||
<img svg-inline src="../assets/icons/maps/initialState.svg">
|
||||
</template>
|
||||
<template slot="description">
|
||||
<p class="text is-caption is-txtGrey" v-html="infoText"></p>
|
||||
</template>
|
||||
<template slot="actionButton">
|
||||
<CreateButton visualizationType="maps" :disabled="isViewer">{{ buttonText }}</CreateButton>
|
||||
</template>
|
||||
</InitialState>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CreateButton from 'new-dashboard/components/CreateButton.vue';
|
||||
import InitialState from 'new-dashboard/components/States/InitialState';
|
||||
|
||||
export default {
|
||||
name: 'CreateMapCard',
|
||||
components: {
|
||||
CreateButton,
|
||||
InitialState
|
||||
},
|
||||
computed: {
|
||||
headerText () {
|
||||
return this.$t(`MapsPage.zeroCase.title`);
|
||||
},
|
||||
infoText () {
|
||||
return this.$t(`MapsPage.zeroCase.description`);
|
||||
},
|
||||
buttonText () {
|
||||
return this.$t(`MapsPage.zeroCase.createMap`);
|
||||
},
|
||||
isViewer () {
|
||||
return this.$store.getters['user/isViewer'];
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,517 @@
|
||||
<template>
|
||||
<a :href="vizUrl"
|
||||
target="_blank"
|
||||
class="dataset-row"
|
||||
:class="{
|
||||
'dataset-row--selected': isSelected,
|
||||
'dataset-row--quick-actions-open': areQuickActionsOpen,
|
||||
'dataset-row--no-hover': !activeHover,
|
||||
'dataset-row--can-hover': canHover
|
||||
}"
|
||||
@click="onClick">
|
||||
|
||||
<div class="viz-column--main-info">
|
||||
<div class="cell cell--start cell--first">
|
||||
<div class="row-dataType">
|
||||
<div class="icon--dataType" :class="`icon--${dataType}`"></div>
|
||||
</div>
|
||||
<span class="checkbox row-checkbox" @mouseover="mouseOverChildElement" @mouseleave="mouseOutChildElement">
|
||||
<input class="checkbox-input" :class="{ 'is-selected': isSelected }" @click.stop.prevent="toggleSelection($event)" type="checkbox">
|
||||
<span class="checkbox-decoration">
|
||||
<img svg-inline src="../../assets/icons/common/checkbox.svg">
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="cell cell--main">
|
||||
<div class="title-container" @mouseover="showCopyDropdown" @mouseleave="hideCopyDropdown">
|
||||
<h3 class="text is-caption is-txtGrey u-ellipsis row-title" :title="dataset.name">
|
||||
{{ dataset.name }}
|
||||
</h3>
|
||||
<div class="dropdown-container" @mouseover="mouseOverChildElement" @mouseleave="mouseOutChildElement">
|
||||
<CopyDropdown :textToCopy="dataset.name" :isVisible="copyDropdownVisible" @hideDropdown="hideCopyDropdown"></CopyDropdown>
|
||||
</div>
|
||||
<span v-if="showInteractiveElements" class="card-favorite" :class="{'is-favorite': dataset.liked}" @click.prevent="toggleFavorite" @mouseover="mouseOverChildElement" @mouseleave="mouseOutChildElement">
|
||||
<img svg-inline src="../../assets/icons/common/favorite.svg">
|
||||
</span>
|
||||
</div>
|
||||
<div class="row-metadataContainer" v-if="hasTags || isSharedWithMe || isSharedWithColleagues">
|
||||
<div class="row-metadata" v-if="hasTags" @mouseover="mouseOverChildElement" @mouseleave="mouseOutChildElement">
|
||||
<img class="icon-metadata" src="../../assets/icons/common/tag.svg" width="14" height="14">
|
||||
<ul v-if="tagsChars <= maxTagChars" class="tag-list">
|
||||
<li v-for="(tag, index) in dataset.tags" :key="tag">
|
||||
<router-link :to="{ name: 'tagSearch', params: { tag } }" class="text is-small is-txtSoftGrey tag-list__tag">{{ tag }}</router-link><span class="text is-small is-txtSoftGrey" v-if="!isLastTag(index)">, </span>
|
||||
</li>
|
||||
</ul>
|
||||
<FeaturesDropdown v-if="tagsChars > maxTagChars" :list=dataset.tags linkRoute="tagSearch" feature="tag">
|
||||
<span class="tag-list__more-tags text is-small is-txtSoftGrey">{{numberTags}} {{$t(`DatasetCard.tags`)}}</span>
|
||||
</FeaturesDropdown>
|
||||
</div>
|
||||
<div class="row-metadata" v-if="isSharedWithMe">
|
||||
<img class="icon-metadata" svg-inline src="../../assets/icons/common/user.svg">
|
||||
<span class="text is-small is-txtSoftGrey">{{dataset.permission.owner.username}}</span>
|
||||
</div>
|
||||
<SharedBrief class="row-metadata u-ellipsis" v-if="isSharedWithColleagues && !isSharedWithMe" :colleagues="colleaguesSharedList" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="viz-column--extra-info">
|
||||
<div class="viz-column--status">
|
||||
<div class="cell cell--large">
|
||||
<span class="text is-small is-txtSoftGrey">{{ lastUpdated }}</span>
|
||||
</div>
|
||||
<div class="cell cell--xsmall u-txt-right">
|
||||
<span class="text is-small is-txtSoftGrey" :title="$tc('DatasetCard.numberRows', dataset.table.row_count, { count: getNumberInLocaleFormat(dataset.table.row_count) })">
|
||||
{{ numberFormatter(dataset.table.row_count) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="cell cell--small u-txt-right">
|
||||
<span class="text is-small is-txtSoftGrey">{{ humanFileSize(dataset.table.size) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="viz-column--share">
|
||||
<div class="cell cell--small">
|
||||
<span class="text is-small is-txtSoftGrey" v-if="!dataset.dependent_visualizations_count">
|
||||
{{ $tc('DatasetCard.maps', 0) }}
|
||||
</span>
|
||||
<FeaturesDropdown :list="dependentVisualizationsWithUrl" v-if="dataset.dependent_visualizations_count" @mouseover.native="mouseOverChildElement" @mouseleave.native="mouseOutChildElement">
|
||||
<span class="text is-small is-txtSoftGrey dataset__dependent-visualizations">
|
||||
{{ $tc('DatasetCard.maps', dataset.dependent_visualizations_count) }}
|
||||
</span>
|
||||
|
||||
<template slot="footer" v-if="dataset.dependent_visualizations_count > 10">
|
||||
+ {{ $tc('DatasetCard.maps', dataset.dependent_visualizations_count - 10) }}
|
||||
</template>
|
||||
</FeaturesDropdown>
|
||||
</div>
|
||||
<div class="cell cell--small">
|
||||
<span class="text is-small is-txtSoftGrey">{{ $t(`DatasetCard.shared.${dataset.privacy}`) }}</span>
|
||||
</div>
|
||||
<div class="cell cell--last" @mouseover="mouseOverChildElement" @mouseleave="mouseOutChildElement">
|
||||
<span class="quick-actions-placeholder" v-if="!showInteractiveElements"></span>
|
||||
<DatasetQuickActions
|
||||
v-if="showInteractiveElements"
|
||||
:dataset="dataset"
|
||||
:isSharedWithMe="isSharedWithMe"
|
||||
class="dataset--quick-actions"
|
||||
@open="openQuickActions"
|
||||
@close="closeQuickActions"
|
||||
@contentChanged="onContentChanged"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DatasetQuickActions from 'new-dashboard/components/QuickActions/DatasetQuickActions';
|
||||
import CopyDropdown from 'new-dashboard/components/Dropdowns/CopyDropdown';
|
||||
import distanceInWordsStrict from 'date-fns/distance_in_words_strict';
|
||||
import * as Visualization from 'new-dashboard/core/models/visualization';
|
||||
import FeaturesDropdown from '../Dropdowns/FeaturesDropdown';
|
||||
import SharedBrief from 'new-dashboard/components/SharedBrief';
|
||||
import countCharsArray from 'new-dashboard/utils/count-chars-array';
|
||||
import * as Formatter from 'new-dashboard/utils/formatter';
|
||||
import zhCN from 'date-fns/locale/zh_cn';
|
||||
|
||||
|
||||
export default {
|
||||
name: 'DatasetCard',
|
||||
components: {
|
||||
DatasetQuickActions,
|
||||
FeaturesDropdown,
|
||||
SharedBrief,
|
||||
CopyDropdown
|
||||
},
|
||||
props: {
|
||||
dataset: Object,
|
||||
isSelected: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
canHover: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
selectMode: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
storeActionType: {
|
||||
type: String,
|
||||
default: 'datasets'
|
||||
}
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
areQuickActionsOpen: false,
|
||||
activeHover: true,
|
||||
copyDropdownVisible: false,
|
||||
maxTags: 3,
|
||||
maxTagChars: 30
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
lastUpdated () {
|
||||
if (this.$props.dataset.synchronization && this.$props.dataset.synchronization.updated_at) {
|
||||
return this.$t('DatasetCard.lastSynced', { date: distanceInWordsStrict(this.$props.dataset.synchronization.updated_at, new Date(),{ locale: zhCN }) });
|
||||
} else {
|
||||
return this.$t('DatasetCard.lastUpdated', { date: distanceInWordsStrict(this.$props.dataset.updated_at, new Date(),{ locale: zhCN }) });
|
||||
}
|
||||
},
|
||||
dataType () {
|
||||
const geometryTypes = {
|
||||
'st_multipolygon': 'polygon',
|
||||
'st_polygon': 'polygon',
|
||||
'st_multilinestring': 'line',
|
||||
'st_linestring': 'line',
|
||||
'st_multipoint': 'point',
|
||||
'st_point': 'point',
|
||||
'': 'empty'
|
||||
};
|
||||
let geometry = '';
|
||||
if (this.$props.dataset.table && this.$props.dataset.table.geometry_types && this.$props.dataset.table.geometry_types.length) {
|
||||
geometry = this.$props.dataset.table.geometry_types[0];
|
||||
}
|
||||
const currentGeometryType = geometry.toLowerCase();
|
||||
return geometryTypes[currentGeometryType] ? geometryTypes[currentGeometryType] : 'unknown';
|
||||
},
|
||||
numberTags () {
|
||||
return this.$props.dataset.tags ? this.$props.dataset.tags.length : 0;
|
||||
},
|
||||
tagsChars () {
|
||||
return countCharsArray(this.$props.dataset.tags, ', ');
|
||||
},
|
||||
hasTags () {
|
||||
return this.numberTags > 0;
|
||||
},
|
||||
isSharedWithMe () {
|
||||
return Visualization.isSharedWithMe(this.$props.dataset, this.$cartoModels);
|
||||
},
|
||||
showInteractiveElements () {
|
||||
return !this.$props.selectMode;
|
||||
},
|
||||
dependentVisualizationsWithUrl () {
|
||||
return this.$props.dataset.dependent_visualizations.map(visualization => {
|
||||
visualization.url = Visualization.getURL(visualization, this.$cartoModels);
|
||||
return visualization;
|
||||
});
|
||||
},
|
||||
vizUrl () {
|
||||
return Visualization.getURL(this.$props.dataset, this.$cartoModels);
|
||||
},
|
||||
colleaguesSharedList () {
|
||||
return this.$props.dataset.permission.acl;
|
||||
},
|
||||
isSharedWithColleagues () {
|
||||
return this.$props.dataset.permission.acl.length > 0;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleSelection ($event) {
|
||||
this.$emit('toggleSelection', {
|
||||
dataset: this.$props.dataset,
|
||||
isSelected: !this.$props.isSelected,
|
||||
event: $event
|
||||
});
|
||||
},
|
||||
getNumberInLocaleFormat (number) {
|
||||
return number.toLocaleString();
|
||||
},
|
||||
humanFileSize (size) {
|
||||
return Formatter.humanFileSize(size);
|
||||
},
|
||||
numberFormatter (size) {
|
||||
return Formatter.numberFormatter(size);
|
||||
},
|
||||
toggleFavorite () {
|
||||
if (this.$props.dataset.liked) {
|
||||
this.deleteLikeDataset(this.$props.dataset);
|
||||
} else {
|
||||
this.likeDataset(this.$props.dataset);
|
||||
}
|
||||
},
|
||||
isLastTag (index) {
|
||||
return index === this.numberTags - 1;
|
||||
},
|
||||
mouseOverChildElement () {
|
||||
this.activeHover = false;
|
||||
},
|
||||
mouseOutChildElement () {
|
||||
this.activeHover = true;
|
||||
},
|
||||
showCopyDropdown () {
|
||||
this.copyDropdownVisible = true;
|
||||
},
|
||||
hideCopyDropdown () {
|
||||
this.copyDropdownVisible = false;
|
||||
},
|
||||
openQuickActions () {
|
||||
this.areQuickActionsOpen = true;
|
||||
},
|
||||
closeQuickActions () {
|
||||
this.areQuickActionsOpen = false;
|
||||
},
|
||||
onClick (event) {
|
||||
if (this.$props.selectMode) {
|
||||
event.preventDefault();
|
||||
this.toggleSelection(event);
|
||||
}
|
||||
},
|
||||
onContentChanged (type) {
|
||||
this.$emit('contentChanged', type);
|
||||
},
|
||||
likeDataset (dataset) {
|
||||
this.$store.dispatch(`${this.storeActionType}/like`, dataset);
|
||||
},
|
||||
deleteLikeDataset (dataset) {
|
||||
this.$store.dispatch(`${this.storeActionType}/deleteLike`, dataset);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import 'new-dashboard/styles/variables';
|
||||
|
||||
.dataset-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
padding: 0 14px;
|
||||
background-color: $white;
|
||||
|
||||
&.dataset-row--selected {
|
||||
box-shadow: 0 0 0 1px $primary-color;
|
||||
}
|
||||
|
||||
&.dataset-row--quick-actions-open,
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
|
||||
.dataset--quick-actions {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $softblue;
|
||||
|
||||
&:not(.dataset-row--no-hover) {
|
||||
.row-title {
|
||||
color: $primary-color;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.tag-list {
|
||||
.tag-list__tag {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.dataset__dependent-visualizations {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.card-favorite {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.tag-list__more-tags {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&.dataset-row--selected {
|
||||
&.dataset-row--can-hover {
|
||||
.row-dataType {
|
||||
transform: translateY(-100%);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.row-checkbox {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
pointer-events: all;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tag-list {
|
||||
display: flex;
|
||||
|
||||
li {
|
||||
margin-right: 0.2em;
|
||||
}
|
||||
|
||||
.tag-list__tag {
|
||||
&:hover {
|
||||
color: $primary-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tag-list__more-tags {
|
||||
&:hover {
|
||||
color: $primary-color;
|
||||
}
|
||||
}
|
||||
|
||||
.row-dataType {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
margin-right: 6px;
|
||||
margin-left: 6px;
|
||||
padding: 9px;
|
||||
overflow: hidden;
|
||||
transition: all 0.25s cubic-bezier(0.4, 0.01, 0.165, 0.99);
|
||||
border-radius: 2px;
|
||||
background-color: $thumbnail__bg-color;
|
||||
|
||||
.row-typeIcon {
|
||||
fill: $text__color;
|
||||
}
|
||||
|
||||
&.row-dataType--premium {
|
||||
background-color: $color-premium;
|
||||
|
||||
.row-typeIcon {
|
||||
fill: $white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.row-checkbox {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
transform: translateY(250%);
|
||||
transition: all 0.25s cubic-bezier(0.4, 0.01, 0.165, 0.99);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.cell--start {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.icon--dataType {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
|
||||
&.icon--point {
|
||||
background-image: url("../../assets/icons/datasets/data-types/dots.svg");
|
||||
}
|
||||
|
||||
&.icon--polygon {
|
||||
background-image: url("../../assets/icons/datasets/data-types/area.svg");
|
||||
}
|
||||
|
||||
&.icon--line {
|
||||
background-image: url("../../assets/icons/datasets/data-types/line.svg");
|
||||
}
|
||||
|
||||
&.icon--empty,
|
||||
&.icon--unknown {
|
||||
background-image: url("../../assets/icons/datasets/data-types/unknown.svg");
|
||||
}
|
||||
}
|
||||
|
||||
.title-container {
|
||||
display: flex;
|
||||
position: relative;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dropdown-container {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
bottom: 100%;
|
||||
margin-left: 32px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.card-favorite {
|
||||
margin-left: 8px;
|
||||
opacity: 0;
|
||||
|
||||
svg {
|
||||
transform: translateY(2px);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.favorite-icon {
|
||||
stroke: $primary-color;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-favorite {
|
||||
opacity: 1;
|
||||
|
||||
.favorite-icon {
|
||||
stroke: #FFC300;
|
||||
fill: #FFC300;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.favorite-icon {
|
||||
stroke: $primary-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dataset--quick-actions {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.quick-actions-placeholder {
|
||||
display: block;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.row-metadataContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 4px;
|
||||
|
||||
.row-metadata {
|
||||
margin-left: 16px;
|
||||
|
||||
&:first-of-type {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.icon-metadata,
|
||||
.features-dropdown,
|
||||
ul,
|
||||
li {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.icon-metadata {
|
||||
margin-right: 4px;
|
||||
transform: translate(0, 2px);
|
||||
}
|
||||
|
||||
li {
|
||||
margin-right: 0.2em;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<div class="dataset-fake-row">
|
||||
<div class="viz-column--main-info">
|
||||
<div class="cell-placeholder thumbnail">
|
||||
<div class="icon-placeholder"/>
|
||||
</div>
|
||||
<div class="cell-placeholder cell--main">
|
||||
<div class="text-placeholder"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="viz-column--extra-info">
|
||||
<div class="viz-column--status">
|
||||
<div class="cell-placeholder cell--large">
|
||||
<div class="text-placeholder"/>
|
||||
</div>
|
||||
<div class="cell-placeholder cell--xsmall">
|
||||
<div class="text-placeholder"/>
|
||||
</div>
|
||||
<div class="cell-placeholder cell--small">
|
||||
<div class="text-placeholder"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="viz-column--share">
|
||||
<div class="cell-placeholder cell--small">
|
||||
<div class="text-placeholder"/>
|
||||
</div>
|
||||
<div class="cell-placeholder cell--small">
|
||||
<div class="text-placeholder"/>
|
||||
</div>
|
||||
<div class="cell-placeholder cell--quick-actions">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'DatasetCardFake'
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import 'new-dashboard/styles/variables';
|
||||
|
||||
.dataset-fake-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 81px;
|
||||
padding: 22px 14px;
|
||||
background-color: $white;
|
||||
}
|
||||
|
||||
.icon-placeholder {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 2px;
|
||||
background-color: $softblue;
|
||||
}
|
||||
|
||||
.text-placeholder {
|
||||
height: 24px;
|
||||
background-color: $softblue;
|
||||
}
|
||||
|
||||
.cell-placeholder {
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.cell--main {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.cell--quick-actions {
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
padding-right: 16px;
|
||||
padding-left: 6px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,179 @@
|
||||
<template>
|
||||
<div class="dataset-list-row">
|
||||
<div class="viz-column--main-info">
|
||||
<div class="dataset-list-cell cell cell--start">
|
||||
</div>
|
||||
<div class="dataset-list-cell cell cell--main" @click="changeOrder('name')">
|
||||
<span class="text is-small is-txtSoftGrey"
|
||||
:class="{
|
||||
'is-active': isOrderApplied('name'),
|
||||
'is-reversed': isReverseOrderApplied('name'),
|
||||
'element-sort': isSortable
|
||||
}">
|
||||
{{ $t(`DatasetListHeader.name`) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="viz-column--extra-info">
|
||||
<div class="viz-column--status">
|
||||
<div class="dataset-list-cell cell cell--large" @click="changeOrder('updated_at')">
|
||||
<span class="text is-small is-txtSoftGrey"
|
||||
:class="{
|
||||
'is-active': isOrderApplied('updated_at'),
|
||||
'is-reversed': isReverseOrderApplied('updated_at'),
|
||||
'element-sort': isSortable
|
||||
}">
|
||||
{{ $t(`DatasetListHeader.lastModified`) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="dataset-list-cell cell cell--xsmall u-txt-right" @click="changeOrder('estimated_row_count')">
|
||||
<span class="text is-small is-txtSoftGrey"
|
||||
:class="{
|
||||
'is-active': isOrderApplied('estimated_row_count'),
|
||||
'is-reversed': isReverseOrderApplied('estimated_row_count'),
|
||||
'element-sort': isSortable
|
||||
}">
|
||||
{{ $t(`DatasetListHeader.rows`) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="dataset-list-cell cell cell--small u-txt-right" @click="changeOrder('size')">
|
||||
<span class="text is-small is-txtSoftGrey"
|
||||
:class="{
|
||||
'is-active': isOrderApplied('size'),
|
||||
'is-reversed': isReverseOrderApplied('size'),
|
||||
'element-sort': isSortable
|
||||
}">
|
||||
{{ $t(`DatasetListHeader.size`) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="viz-column--share">
|
||||
<div class="dataset-list-cell cell cell--small" @click="changeOrder('dependent_visualizations')">
|
||||
<span class="text is-small is-txtSoftGrey"
|
||||
:class="{
|
||||
'is-active': isOrderApplied('dependent_visualizations'),
|
||||
'is-reversed': isReverseOrderApplied('dependent_visualizations'),
|
||||
'element-sort': isSortable
|
||||
}">
|
||||
{{ $t(`DatasetListHeader.usage`) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="dataset-list-cell cell cell--small" @click="changeOrder('privacy')">
|
||||
<span class="text is-small is-txtSoftGrey"
|
||||
:class="{
|
||||
'is-active': isOrderApplied('privacy'),
|
||||
'is-reversed': isReverseOrderApplied('privacy'),
|
||||
'element-sort': isSortable
|
||||
}">
|
||||
{{ $t(`DatasetListHeader.privacy`) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="dataset-list-cell cell cell--end"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'DatasetListHeader',
|
||||
props: {
|
||||
order: String,
|
||||
orderDirection: String,
|
||||
isSortable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
changeOrder (order) {
|
||||
if (this.order === order) {
|
||||
return this.setOrder(
|
||||
order,
|
||||
this.getOppositeOrderDirection(this.orderDirection)
|
||||
);
|
||||
}
|
||||
|
||||
this.setOrder(order);
|
||||
},
|
||||
|
||||
getOppositeOrderDirection (order) {
|
||||
if (order === 'desc') {
|
||||
return 'asc';
|
||||
}
|
||||
|
||||
if (order === 'asc') {
|
||||
return 'desc';
|
||||
}
|
||||
},
|
||||
|
||||
isOrderApplied (order) {
|
||||
return order === this.order;
|
||||
},
|
||||
|
||||
isReverseOrderApplied (order) {
|
||||
return order === this.order && this.orderDirection === 'asc';
|
||||
},
|
||||
|
||||
setOrder (order, direction = 'desc') {
|
||||
this.$emit('changeOrder', { order, direction });
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import 'new-dashboard/styles/variables';
|
||||
|
||||
.dataset-list-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin-bottom: 1px;
|
||||
padding: 14px;
|
||||
overflow: hidden;
|
||||
border-bottom: 1px solid $softblue;
|
||||
background-color: $white;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.cell--start {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-self: flex-start;
|
||||
width: 58px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.cell--end {
|
||||
width: 34px;
|
||||
}
|
||||
|
||||
.element-sort {
|
||||
&.is-active,
|
||||
&:hover {
|
||||
color: $text__color;
|
||||
cursor: pointer;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 14px;
|
||||
height: 100%;
|
||||
margin-left: 4px;
|
||||
transition: all 0.25s cubic-bezier(0.4, 0.01, 0.165, 0.99);
|
||||
background-image: url('../../assets/icons/datasets/chevron.svg');
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
&.is-reversed {
|
||||
&::after {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,321 @@
|
||||
<template>
|
||||
<div class="container grid">
|
||||
<div class="full-width">
|
||||
<SectionTitle class="grid-cell" :showActionButton="!selectedDatasets.length" ref="headerContainer">
|
||||
<template slot="icon">
|
||||
<img src="../assets/icons/section-title/data.svg" width="18" height="20" />
|
||||
</template>
|
||||
|
||||
<template slot="title">
|
||||
<VisualizationsTitle
|
||||
:defaultTitle="$t(`DataPage.header.title['${appliedFilter}']`)"
|
||||
:selectedItems="selectedDatasets.length"
|
||||
:vizQuota="datasetsQuota"
|
||||
:vizCount="datasetsCount"
|
||||
:isOutOfQuota="isOutOfDatasetsQuota"/>
|
||||
</template>
|
||||
|
||||
<template v-if="shouldShowLimitsWarning" slot="warning">
|
||||
<BadgeWarning>
|
||||
<div class="warning" v-html="$t('DataPage.header.warning', { counter: `${datasetsCount}/${datasetsQuota}`, path: upgradeUrl })"></div>
|
||||
</BadgeWarning>
|
||||
</template>
|
||||
|
||||
<template slot="dropdownButton">
|
||||
<DatasetBulkActions
|
||||
:selectedDatasets="selectedDatasets"
|
||||
:areAllDatasetsSelected="areAllDatasetsSelected"
|
||||
v-if="hasBulkActions && selectedDatasets.length"
|
||||
@selectAll="selectAll"
|
||||
@deselectAll="deselectAll"></DatasetBulkActions>
|
||||
|
||||
<SettingsDropdown
|
||||
section="datasets"
|
||||
v-if="!selectedDatasets.length"
|
||||
:filter="appliedFilter"
|
||||
:order="appliedOrder"
|
||||
:orderDirection="appliedOrderDirection"
|
||||
:metadata="datasetsMetadata"
|
||||
@filterChanged="applyFilter">
|
||||
<img svg-inline src="../assets/icons/common/filter.svg">
|
||||
</SettingsDropdown>
|
||||
</template>
|
||||
|
||||
<template slot="actionButton" v-if="!isFirstTimeViewingDashboard && !selectedDatasets.length">
|
||||
<CreateButton visualizationType="dataset" :disabled="!canCreateDatasets">
|
||||
{{ $t(`DataPage.createDataset`) }}
|
||||
</CreateButton>
|
||||
</template>
|
||||
</SectionTitle>
|
||||
</div>
|
||||
|
||||
<div class="grid-cell grid-cell--col12" v-if="initialState">
|
||||
<InitialState :title="$t(`DataPage.zeroCase.title`)">
|
||||
<template slot="icon">
|
||||
<img svg-inline src="../assets/icons/datasets/initialState.svg">
|
||||
</template>
|
||||
<template slot="description">
|
||||
<p class="text is-caption is-txtGrey" v-html="$t(`DataPage.zeroCase.description`)"></p>
|
||||
</template>
|
||||
<template slot="actionButton">
|
||||
<CreateButton visualizationType="dataset" :disabled="!canCreateDatasets">{{ $t(`DataPage.zeroCase.createDataset`) }}</CreateButton>
|
||||
</template>
|
||||
</InitialState>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="shouldShowHeader"
|
||||
class="grid-cell grid-cell--noMargin grid-cell--col12 grid__head--sticky"
|
||||
:class="{
|
||||
'has-user-notification': isNotificationVisible,
|
||||
'in-home': isInHomePage,
|
||||
}">
|
||||
|
||||
<DatasetListHeader :order="appliedOrder" :orderDirection="appliedOrderDirection" @changeOrder="applyOrder"></DatasetListHeader>
|
||||
</div>
|
||||
|
||||
<ul class="grid-cell grid-cell--col12" v-if="!isFetchingDatasets && currentEntriesCount > 0">
|
||||
<li v-for="dataset in datasets" :key="dataset.id" class="dataset-item">
|
||||
<DatasetCard
|
||||
:dataset="dataset"
|
||||
:isSelected="isDatasetSelected(dataset)"
|
||||
:selectMode="isSomeDatasetSelected"
|
||||
:canHover="canHoverCard"
|
||||
@toggleSelection="toggleSelected"
|
||||
@contentChanged="onContentChanged">
|
||||
</DatasetCard>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="grid-cell grid-cell--col12">
|
||||
<EmptyState
|
||||
:text="emptyStateText"
|
||||
v-if="emptyState">
|
||||
<img svg-inline src="../assets/icons/common/compass.svg">
|
||||
</EmptyState>
|
||||
</div>
|
||||
|
||||
<ul v-if="isFetchingDatasets" class="grid-cell grid-cell--col12">
|
||||
<li v-for="n in maxVisibleDatasets" :key="n" class="dataset-item">
|
||||
<DatasetCardFake></DatasetCardFake>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapGetters } from 'vuex';
|
||||
import DatasetCard from '../components/Dataset/DatasetCard';
|
||||
import DatasetListHeader from '../components/Dataset/DatasetListHeader';
|
||||
import DatasetCardFake from '../components/Dataset/DatasetCardFake';
|
||||
import SettingsDropdown from '../components/Settings/Settings';
|
||||
import SectionTitle from 'new-dashboard/components/SectionTitle';
|
||||
import VisualizationsTitle from 'new-dashboard/components/VisualizationsTitle';
|
||||
import BadgeWarning from 'new-dashboard/components/BadgeWarning';
|
||||
import InitialState from 'new-dashboard/components/States/InitialState';
|
||||
import EmptyState from 'new-dashboard/components/States/EmptyState';
|
||||
import CreateButton from 'new-dashboard/components/CreateButton';
|
||||
import DatasetBulkActions from 'new-dashboard/components/BulkActions/DatasetBulkActions.vue';
|
||||
import { shiftClick } from 'new-dashboard/utils/shift-click.service.js';
|
||||
|
||||
export default {
|
||||
name: 'DatasetsList',
|
||||
props: {
|
||||
maxVisibleDatasets: {
|
||||
type: Number,
|
||||
default: 12
|
||||
},
|
||||
hasBulkActions: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
canHoverCard: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
components: {
|
||||
CreateButton,
|
||||
SettingsDropdown,
|
||||
SectionTitle,
|
||||
VisualizationsTitle,
|
||||
BadgeWarning,
|
||||
DatasetCard,
|
||||
DatasetCardFake,
|
||||
InitialState,
|
||||
EmptyState,
|
||||
DatasetListHeader,
|
||||
DatasetBulkActions
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
isScrollPastHeader: false,
|
||||
selectedDatasets: [],
|
||||
lastCheckedItem: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
appliedFilter: state => state.datasets.filterType,
|
||||
appliedOrder: state => state.datasets.order,
|
||||
appliedOrderDirection: state => state.datasets.orderDirection,
|
||||
datasets: state => state.datasets.list,
|
||||
datasetsMetadata: state => state.datasets.metadata,
|
||||
isFetchingDatasets: state => state.datasets.isFetching,
|
||||
filterType: state => state.datasets.filterType,
|
||||
currentEntriesCount: state => state.datasets.metadata.total_entries,
|
||||
totalUserEntries: state => state.datasets.metadata.total_user_entries,
|
||||
totalShared: state => state.datasets.metadata.total_shared,
|
||||
isFirstTimeViewingDashboard: state => state.config.isFirstTimeViewingDashboard,
|
||||
upgradeUrl: state => state.config.upgrade_url
|
||||
}),
|
||||
...mapGetters({
|
||||
datasetsCount: 'user/datasetsCount',
|
||||
datasetsQuota: 'user/datasetsQuota',
|
||||
isOutOfDatasetsQuota: 'user/isOutOfDatasetsQuota'
|
||||
}),
|
||||
canCreateDatasets () {
|
||||
return this.$store.getters['user/canCreateDatasets'];
|
||||
},
|
||||
areAllDatasetsSelected () {
|
||||
return Object.keys(this.datasets).length === this.selectedDatasets.length;
|
||||
},
|
||||
shouldShowHeader () {
|
||||
return !this.emptyState && !this.initialState;
|
||||
},
|
||||
initialState () {
|
||||
return this.isFirstTimeViewingDashboard &&
|
||||
!this.hasSharedDatasets &&
|
||||
!this.isFetchingDatasets &&
|
||||
this.hasFilterApplied('mine') &&
|
||||
this.totalUserEntries <= 0;
|
||||
},
|
||||
emptyState () {
|
||||
return ((!this.isFirstTimeViewingDashboard || this.hasSharedDatasets) || this.isFirstTimeViewerAfterAction) &&
|
||||
!this.isFetchingDatasets &&
|
||||
!this.currentEntriesCount;
|
||||
},
|
||||
emptyStateText () {
|
||||
const route = this.$router.resolve({name: 'datasets', params: { filter: 'shared' }});
|
||||
return this.hasSharedDatasets ? this.$t('DataPage.emptyCase.onlyShared', { path: route.href }) : this.$t('DataPage.emptyCase.default', { path: route.href });
|
||||
},
|
||||
isFirstTimeViewerAfterAction () {
|
||||
// First time viewing dashboard but user has performed any action such as drag and dropping a dataset (no page refreshing)
|
||||
return this.isFirstTimeViewingDashboard && this.currentEntriesCount <= 0 && !this.hasFilterApplied('mine');
|
||||
},
|
||||
hasSharedDatasets () {
|
||||
return this.totalShared > 0;
|
||||
},
|
||||
isSomeDatasetSelected () {
|
||||
return this.selectedDatasets.length > 0;
|
||||
},
|
||||
shouldShowLimitsWarning () {
|
||||
return !this.selectedDatasets.length && this.isOutOfDatasetsQuota;
|
||||
},
|
||||
isNotificationVisible () {
|
||||
return this.$store.getters['user/isNotificationVisible'];
|
||||
},
|
||||
isInHomePage () {
|
||||
return this.$router.currentRoute.name === 'home';
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
fetchDatasets () {
|
||||
this.$store.dispatch('datasets/fetch');
|
||||
},
|
||||
applyFilter (filter) {
|
||||
this.$emit('applyFilter', filter);
|
||||
},
|
||||
applyOrder (orderParams) {
|
||||
this.$emit('applyOrder', orderParams);
|
||||
},
|
||||
toggleSelected ({ dataset, isSelected, event }) {
|
||||
if (this.selectedDatasets.length && event.shiftKey) {
|
||||
this.doShiftClick(dataset);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSelected) {
|
||||
this.lastCheckedItem = dataset;
|
||||
this.selectedDatasets.push(dataset);
|
||||
return;
|
||||
}
|
||||
|
||||
this.selectedDatasets = this.selectedDatasets.filter(selectedDataset => selectedDataset.id !== dataset.id);
|
||||
},
|
||||
doShiftClick (dataset) {
|
||||
const datasetsArray = [...Object.values(this.datasets)];
|
||||
this.selectedDatasets = shiftClick(datasetsArray, this.selectedDatasets, dataset, this.lastCheckedItem || dataset);
|
||||
},
|
||||
selectAll () {
|
||||
this.selectedDatasets = [...Object.values(this.$store.state.datasets.list)];
|
||||
},
|
||||
deselectAll () {
|
||||
this.selectedDatasets = [];
|
||||
},
|
||||
isDatasetSelected (dataset) {
|
||||
return this.selectedDatasets.some(selectedDataset => selectedDataset.id === dataset.id);
|
||||
},
|
||||
hasFilterApplied (filter) {
|
||||
return this.filterType === filter;
|
||||
},
|
||||
getHeaderContainer () {
|
||||
return this.$refs.headerContainer;
|
||||
},
|
||||
onContentChanged (type) {
|
||||
this.$emit('contentChanged', type);
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
selectedDatasets () {
|
||||
this.$emit('selectionChange', this.selectedDatasets);
|
||||
},
|
||||
totalUserEntries () {
|
||||
this.$store.dispatch('user/updateTableCount', this.totalUserEntries);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import 'new-dashboard/styles/variables';
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.grid__head--sticky {
|
||||
top: $header__height + $subheader__height;
|
||||
|
||||
&.in-home {
|
||||
top: $header__height;
|
||||
}
|
||||
|
||||
&.has-user-notification {
|
||||
top: $header__height + $subheader__height + $notification-warning__height;
|
||||
|
||||
&.in-home {
|
||||
top: $header__height + $notification-warning__height;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-element {
|
||||
margin-top: 64px;
|
||||
}
|
||||
|
||||
.dataset-item {
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px solid $border-color;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
margin: 20vh 0 8vh;
|
||||
}
|
||||
|
||||
.warning {
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<div class="dropdown" v-if="isVisible" @click.prevent="copyName">
|
||||
<span class="text is-small copy-text" v-if="!copySuccessful"><img svg-inline class="copy-icon" src="../../assets/icons/common/copy.svg">{{$t(`DatasetCard.copy`)}}</span>
|
||||
<span class="text is-small is-txtGrey" v-if="copySuccessful"><img svg-inline class="copy-icon" src="../../assets/icons/common/copy-success.svg">{{$t(`DatasetCard.copySuccess`)}}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'CopyDropdown',
|
||||
props: {
|
||||
textToCopy: String,
|
||||
isVisible: Boolean
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
copySuccessful: false,
|
||||
hideDropdownTimeout: null
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
isVisible: function (newVal, oldVal) {
|
||||
if (newVal === false) {
|
||||
this.resetDropdown();
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
copyName () {
|
||||
var textArea = document.createElement('textarea');
|
||||
textArea.style.position = 'fixed';
|
||||
textArea.style.opacity = 0;
|
||||
textArea.style.height = 0;
|
||||
textArea.style.width = 0;
|
||||
textArea.value = this.textToCopy;
|
||||
document.body.appendChild(textArea);
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
try {
|
||||
this.copySuccessful = document.execCommand('copy');
|
||||
if (this.copySuccessful) {
|
||||
this.hideDropdownTimeout = setTimeout(() => { this.$emit('hideDropdown'); }, 2000);
|
||||
}
|
||||
} catch (err) {
|
||||
this.copySuccessful = false;
|
||||
} finally {
|
||||
document.body.removeChild(textArea);
|
||||
}
|
||||
},
|
||||
resetDropdown () {
|
||||
this.copySuccessful = false;
|
||||
if (this.hideDropdownTimeout) {
|
||||
clearTimeout(this.hideDropdownTimeout);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'new-dashboard/styles/variables';
|
||||
|
||||
.dropdown {
|
||||
position: relative;
|
||||
padding: 12px 16px 8px;
|
||||
border: 1px solid $border-color;
|
||||
border-radius: 4px;
|
||||
background-color: #FFF;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -8px;
|
||||
left: 24px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
transform: rotate(45deg);
|
||||
border: 1px solid $neutral--200;
|
||||
border-top: none;
|
||||
border-left: none;
|
||||
border-radius: 2px;
|
||||
background-color: #FFF;
|
||||
}
|
||||
}
|
||||
|
||||
.copy-text {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.copy-icon {
|
||||
margin-right: 8px;
|
||||
margin-bottom: -2px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<section v-click-outside="closeDropdown">
|
||||
<button class="dropdown__toggle" :class="{ 'dropdown__toggle--active': isOpen }" @click="toggleDropdown">
|
||||
<slot name="button"/>
|
||||
</button>
|
||||
|
||||
<div class="dropdown" :class="{ 'is-open': isOpen }">
|
||||
<slot />
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Dropdown',
|
||||
data () {
|
||||
return {
|
||||
isOpen: false
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
toggleDropdown () {
|
||||
this.isOpen = !this.isOpen;
|
||||
},
|
||||
closeDropdown () {
|
||||
this.isOpen = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'new-dashboard/styles/variables';
|
||||
|
||||
.dropdown {
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
z-index: $z-index__dropdown;
|
||||
right: 0;
|
||||
width: 316px;
|
||||
margin-top: 8px;
|
||||
overflow: hidden;
|
||||
transition: all 0.25s linear;
|
||||
border: 1px solid $border-color;
|
||||
border-radius: 2px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
|
||||
&.is-open {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown__toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 36px;
|
||||
padding: 0 9px;
|
||||
border-radius: 2px;
|
||||
|
||||
&:hover {
|
||||
background-color: $softblue;
|
||||
}
|
||||
|
||||
&.dropdown__toggle--active {
|
||||
background-color: $primary-color;
|
||||
|
||||
.svgicon {
|
||||
fill: $white;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,109 @@
|
||||
<template>
|
||||
<div class="features-dropdown">
|
||||
<slot />
|
||||
<div class="dropdown-container">
|
||||
<ul class="list">
|
||||
<li class="element" v-for="element in list" :key="element.name || element">
|
||||
<a :href="element.url" class="list-text text is-small" v-if="element.url">{{ element.name }}</a>
|
||||
|
||||
<router-link :to="{ name: linkRoute, params: routeParams(element) }" class="list-text text is-small" v-if="!element.url">
|
||||
{{ element }}
|
||||
</router-link>
|
||||
</li>
|
||||
|
||||
<li class="element" v-if="$slots.footer" @click.stop.prevent="noop">
|
||||
<span class="list-text text is-small footer">
|
||||
<slot name="footer" />
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'FeaturesDropdown',
|
||||
props: {
|
||||
list: Array,
|
||||
feature: String,
|
||||
linkRoute: String
|
||||
},
|
||||
methods: {
|
||||
routeParams (element) {
|
||||
return { [this.feature]: element };
|
||||
},
|
||||
noop () {}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import 'new-dashboard/styles/variables';
|
||||
|
||||
.features-dropdown {
|
||||
&:hover {
|
||||
.dropdown-container {
|
||||
opacity: 1;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.feature-text {
|
||||
color: $primary-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.list-text {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
padding: 10px 22px;
|
||||
}
|
||||
|
||||
.dropdown-container {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
min-width: 60%;
|
||||
padding-top: 16px;
|
||||
transform: translateX(-12px);
|
||||
transform: all 0.3s linear;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.list {
|
||||
position: relative;
|
||||
padding: 0;
|
||||
border: 1px solid $border-color;
|
||||
border-radius: 4px;
|
||||
background-color: $white;
|
||||
}
|
||||
|
||||
.element {
|
||||
&:not(:last-of-type) {
|
||||
border-bottom: 1px solid $softblue;
|
||||
}
|
||||
|
||||
&:first-of-type {
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
left: 24px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
transform: rotate(45deg);
|
||||
border: 1px solid $border-color;
|
||||
border-right: none;
|
||||
border-bottom: none;
|
||||
border-radius: 2px;
|
||||
background-color: $white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
color: $text__color;
|
||||
cursor: default;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<div class="container grid footer">
|
||||
<div class="grid-cell grid-cell--col3 grid-cell--col12--mobile footer-logo">
|
||||
<a href="http://www.onenetcom.cn">
|
||||
<img class="carto-logo" src="../assets/icons/common/cartoLogo.svg">
|
||||
</a>
|
||||
</div>
|
||||
<!--
|
||||
<div class="grid-cell grid-cell--col8 grid-cell--col9--tablet grid-cell--col12--mobile">
|
||||
<div class="footer-block">
|
||||
<a href="https://carto.com/help" class="footer-link" target="_blank">
|
||||
<h4 class="title-link title is-caption is-txtGrey">
|
||||
{{ $t(`Footer.HelpCenter.title`) }}<span class="chevron"><img svg-inline src="../assets/icons/common/chevron.svg"/></span>
|
||||
</h4>
|
||||
<p class="description-link text is-small is-txtSoftGrey">{{ $t(`Footer.HelpCenter.description`) }}</p>
|
||||
</a>
|
||||
<a href="https://carto.com/developers" class="footer-link" target="_blank">
|
||||
<h4 class="title-link title is-caption is-txtGrey">
|
||||
{{ $t(`Footer.DeveloperCenter.title`) }}<span class="chevron"><img svg-inline src="../assets/icons/common/chevron.svg"/></span>
|
||||
</h4>
|
||||
<p class="description-link text is-small is-txtSoftGrey">{{ $t(`Footer.DeveloperCenter.description`) }}</p>
|
||||
</a>
|
||||
</div>
|
||||
<div class="footer-block">
|
||||
<a href="mailto:support@carto.com" class="footer-link">
|
||||
<h4 class="title-link title is-caption is-txtGrey">
|
||||
{{ $t(`Footer.TechSupport.title`) }}<span class="chevron"><img svg-inline src="../assets/icons/common/chevron.svg"/></span>
|
||||
</h4>
|
||||
<p class="description-link text is-small is-txtSoftGrey">{{ $t(`Footer.TechSupport.description`) }}</p>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
-->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'DummyFooter'
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import 'new-dashboard/styles/variables';
|
||||
@import 'new-dashboard/styles/components/_footer';
|
||||
</style>
|
||||
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<header class="dummy-header">
|
||||
<a href="https://carto.com">
|
||||
<img svg-inline src="../assets/icons/common/cartoLogo-white.svg" />
|
||||
</a>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'DummyHeader'
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import 'new-dashboard/styles/variables';
|
||||
|
||||
.dummy-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 60px;
|
||||
padding: 0 64px;
|
||||
background-color: $primary-color;
|
||||
|
||||
@media (max-width: $layout-tablet) {
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
@media (max-width: $layout-mobile) {
|
||||
padding: 0 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<a class="feedback" href="javascript:void(0)" @click.stop.prevent="handleClick">
|
||||
<div class="feedback__icon"></div>
|
||||
|
||||
<div class="feedback__message">
|
||||
<p class="feedback__paragraph text is-small is-semibold">
|
||||
{{ $t('FeedbackMessage.title') }}
|
||||
</p>
|
||||
<p class="feedback__paragraph text is-small">
|
||||
{{ $t('FeedbackMessage.message') }}
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import storageAvailable from 'new-dashboard/utils/is-storage-available';
|
||||
|
||||
export default {
|
||||
name: 'FeedbackPopup',
|
||||
mounted () {
|
||||
if (storageAvailable('localStorage')) {
|
||||
this.markFeedbackPopupAsOpened();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
markFeedbackPopupAsOpened () {
|
||||
window.localStorage.setItem('carto.feedback.popupWasShown', JSON.stringify(true));
|
||||
},
|
||||
handleClick () {
|
||||
this.$emit('feedbackClick');
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'new-dashboard/styles/variables';
|
||||
|
||||
.feedback {
|
||||
display: flex;
|
||||
width: 320px;
|
||||
padding: 16px;
|
||||
border-radius: 4px;
|
||||
background-color: $white;
|
||||
box-shadow: $dropdown__shadow;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
transform: rotate(45deg) translateX(-50%);
|
||||
transform-origin: center center;
|
||||
border-radius: 4px;
|
||||
background-color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
.feedback__icon {
|
||||
position: relative;
|
||||
width: 48px;
|
||||
margin-right: 16px;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
background: $primary-color;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background-image: url('../assets/icons/common/chat-bubble.svg');
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
}
|
||||
|
||||
.feedback__message {
|
||||
flex: 1;
|
||||
color: $text__color;
|
||||
}
|
||||
|
||||
.feedback__paragraph {
|
||||
margin-bottom: 8px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<div class="container grid footer">
|
||||
<div class="grid-cell grid-cell--col3 grid-cell--col12--mobile footer-logo">
|
||||
<a href="http://www.onenetcom.cn">
|
||||
<img class="carto-logo" src="../assets/icons/common/cartoLogo.svg">
|
||||
</a>
|
||||
</div>
|
||||
<div class="grid-cell grid-cell--col8 grid-cell--col9--tablet grid-cell--col12--mobile">
|
||||
<div class="footer-block">
|
||||
<a href="#" class="footer-link" target="_blank">
|
||||
<h4 class="title-link title is-caption is-txtGrey">
|
||||
{{ $t(`Footer.HelpCenter.title`) }}<span class="chevron"><img svg-inline src="../assets/icons/common/chevron.svg"/></span>
|
||||
</h4>
|
||||
<p class="description-link text is-small is-txtSoftGrey">{{ $t(`Footer.HelpCenter.description`) }}</p>
|
||||
</a>
|
||||
<a href="#" class="footer-link" target="_blank">
|
||||
<h4 class="title-link title is-caption is-txtGrey">
|
||||
{{ $t(`Footer.DeveloperCenter.title`) }}<span class="chevron"><img svg-inline src="../assets/icons/common/chevron.svg"/></span>
|
||||
</h4>
|
||||
<p class="description-link text is-small is-txtSoftGrey">{{ $t(`Footer.DeveloperCenter.description`) }}</p>
|
||||
</a>
|
||||
</div>
|
||||
<!--
|
||||
<div class="footer-block">
|
||||
<a href="https://gis.stackexchange.com/questions/tagged/carto" class="footer-link" v-if="isFreeUser" target="_blank">
|
||||
<h4 class="title-link title is-caption is-txtGrey">
|
||||
{{ $t(`Footer.GISStackExchange.title`) }}<span class="chevron"><img svg-inline src="../assets/icons/common/chevron.svg"/></span>
|
||||
</h4>
|
||||
<p class="description-link text is-small is-txtSoftGrey">{{ $t(`Footer.GISStackExchange.description`) }}</p>
|
||||
</a>
|
||||
<a href="mailto:support@carto.com" class="footer-link" v-if="isFreeUser">
|
||||
<h4 class="title-link title is-caption is-txtGrey">
|
||||
{{ $t(`Footer.TechSupport.title`) }}<span class="chevron"><img svg-inline src="../assets/icons/common/chevron.svg"/></span>
|
||||
</h4>
|
||||
<p class="description-link text is-small is-txtSoftGrey">{{ $t(`Footer.TechSupport.description`) }}</p>
|
||||
</a>
|
||||
<a href="mailto:support@carto.com" class="footer-link" v-if="isIndividualUser">
|
||||
<h4 class="title-link title is-caption is-txtGrey">
|
||||
{{ $t(`Footer.DedicatedSupport.title`) }}<span class="chevron"><img svg-inline src="../assets/icons/common/chevron.svg"/></span>
|
||||
</h4>
|
||||
<p class="description-link text is-small is-txtSoftGrey">{{ $t(`Footer.DedicatedSupport.description`) }}</p>
|
||||
</a>
|
||||
<a href="mailto:enterprise-support@carto.com" class="footer-link" v-if="isOrganizationUser">
|
||||
<h4 class="title-link title is-caption is-txtGrey">
|
||||
{{ $t(`Footer.DedicatedSupport.title`) }}<span class="chevron"><img svg-inline src="../assets/icons/common/chevron.svg"/></span>
|
||||
</h4>
|
||||
<p class="description-link text is-small is-txtSoftGrey">{{ $t(`Footer.DedicatedSupport.description`) }}</p>
|
||||
</a>
|
||||
<a :href="`mailto:${organizationMail}`" class="footer-link" v-if="isOrganizationUser && !isOrganizationOwner">
|
||||
<h4 class="title-link title is-caption is-txtGrey">
|
||||
{{ $t(`Footer.OrganizationAdmin.title`) }}<span class="chevron"><img svg-inline src="../assets/icons/common/chevron.svg"/></span>
|
||||
</h4>
|
||||
<p class="description-link text is-small is-txtSoftGrey">{{ $t(`Footer.OrganizationAdmin.description`) }}</p>
|
||||
</a>
|
||||
</div>
|
||||
-->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Footer',
|
||||
props: {
|
||||
user: Object
|
||||
},
|
||||
computed: {
|
||||
userAccountType () {
|
||||
return this.user.account_type.toLowerCase();
|
||||
},
|
||||
isFreeUser () {
|
||||
return this.userAccountType === 'free';
|
||||
},
|
||||
|
||||
isIndividualUser () {
|
||||
const noIndividualUsers = ['internal', 'partner', 'ambassador', 'free'];
|
||||
return !(noIndividualUsers.includes(this.userAccountType) || this.user.organization);
|
||||
},
|
||||
|
||||
isOrganizationUser () {
|
||||
const organization = this.user.organization;
|
||||
return Boolean(organization);
|
||||
},
|
||||
|
||||
isOrganizationOwner () {
|
||||
return this.user.org_admin;
|
||||
},
|
||||
|
||||
organizationMail () {
|
||||
const organization = this.user.organization;
|
||||
return organization.admin_email;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import 'new-dashboard/styles/variables';
|
||||
@import 'new-dashboard/styles/components/_footer';
|
||||
</style>
|
||||
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<iframe ref="iframe" frameborder="0" :height="height" :width="width"></iframe>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'InjectableIframe',
|
||||
mounted () {
|
||||
this.setContent();
|
||||
},
|
||||
props: {
|
||||
content: String,
|
||||
height: String,
|
||||
width: String
|
||||
},
|
||||
methods: {
|
||||
setContent () {
|
||||
const docIframe = this.$refs.iframe.contentWindow.document;
|
||||
docIframe.open('text/html', 'replace');
|
||||
docIframe.write(this.content);
|
||||
docIframe.close();
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
content () {
|
||||
this.setContent();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,325 @@
|
||||
<template>
|
||||
<a :href="vizUrl"
|
||||
target="_blank"
|
||||
class="row"
|
||||
:class="{
|
||||
'row--selected': isSelected,
|
||||
'row--quick-actions-open': areQuickActionsOpen,
|
||||
'row--no-hover': !activeHover,
|
||||
'row--can-hover': canHover
|
||||
}"
|
||||
@click="onClick">
|
||||
|
||||
<div class="viz-column--main-info">
|
||||
<div class="cell cell--thumbnail cell--first">
|
||||
<div class="cell__media" :class="{'has-error': isThumbnailErrored}">
|
||||
<img class="cell__map-thumbnail" :src="mapThumbnailUrl" @error="onThumbnailError" v-if="!isThumbnailErrored"/>
|
||||
|
||||
<div class="MapCard-error" v-if="isThumbnailErrored"></div>
|
||||
</div>
|
||||
|
||||
<span class="checkbox cell__checkbox" @mouseover="mouseOverChildElement" @mouseleave="mouseOutChildElement">
|
||||
<input class="checkbox-input" :class="{'is-selected': isSelected }" @click.stop.prevent="toggleSelection($event)" type="checkbox">
|
||||
<span class="checkbox-decoration">
|
||||
<img svg-inline src="../../assets/icons/common/checkbox.svg">
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="cell cell--map-name cell--main">
|
||||
<div class="cell__title">
|
||||
<h3 class="text is-caption is-txtGrey u-ellipsis cell--map-name__text">
|
||||
{{ visualization.name }}
|
||||
</h3>
|
||||
|
||||
<span v-if="showInteractiveElements" class="cell__favorite" :class="{ 'is-favorite': visualization.liked }" @click.prevent="toggleFavorite" @mouseover="mouseOverChildElement" @mouseleave="mouseOutChildElement">
|
||||
<img svg-inline src="../../assets/icons/common/favorite.svg">
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="metadata" v-if="hasTags || isSharedWithMe || isSharedWithColleagues">
|
||||
<div class="metadata__element" v-if="hasTags" @mouseover="mouseOverChildElement" @mouseleave="mouseOutChildElement">
|
||||
<img class="metadata__icon" src="../../assets/icons/common/tag.svg" width="14" height="14">
|
||||
|
||||
<ul class="metadata__tags" v-if="tagsChars <= maxTagsChars">
|
||||
<li v-for="(tag, index) in visualization.tags" :key="tag">
|
||||
<router-link :to="{ name: 'tagSearch', params: { tag } }" class="text is-small is-txtSoftGrey metadata__tag">{{ tag }}</router-link><span class="text is-small is-txtSoftGrey" v-if="!isLastTag(index)">, </span>
|
||||
</li>
|
||||
</ul>
|
||||
<FeaturesDropdown v-if="tagsChars > maxTagsChars" :list=visualization.tags linkRoute="tagSearch" feature="tag">
|
||||
<span class="metadata__tags-count text is-small is-txtSoftGrey">{{tagsLength}} {{$t(`DatasetCard.tags`)}}</span>
|
||||
</FeaturesDropdown>
|
||||
</div>
|
||||
|
||||
<div class="metadata__element" v-if="isSharedWithMe">
|
||||
<img class="metadata__icon" svg-inline src="../../assets/icons/common/user.svg">
|
||||
<span class="text is-small is-txtSoftGrey">{{visualization.permission.owner.username}}</span>
|
||||
</div>
|
||||
|
||||
<SharedBrief class="metadata__element u-ellipsis" v-if="isSharedWithColleagues && !isSharedWithMe" :colleagues="colleaguesSharedList" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="viz-column--extra-info">
|
||||
<div class="viz-column--status">
|
||||
<div class="cell cell--large">
|
||||
<span class="text is-small is-txtSoftGrey">{{ lastUpdated }}</span>
|
||||
</div>
|
||||
<div class="cell cell--large u-txt-right u-p--0 ">
|
||||
<span class="text is-small is-txtSoftGrey">{{ numberViews }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="viz-column--share">
|
||||
<div class="cell cell--small">
|
||||
<p class="text is-small is-txtSoftGrey">
|
||||
{{ $t(`MapCard.shared.${visualization.privacy}`) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="cell quick-actions cell--last" @mouseover="mouseOverChildElement" @mouseleave="mouseOutChildElement">
|
||||
<div class="quick-actions__placeholder" v-if="!showInteractiveElements || isSharedWithMe"></div>
|
||||
<MapQuickActions class="quick-actions__element" v-if="showInteractiveElements" :map="visualization" @open="openQuickActions" @close="closeQuickActions" @contentChanged="onContentChanged" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import computed from './shared/computed';
|
||||
import data from './shared/data';
|
||||
import FeaturesDropdown from 'new-dashboard/components/Dropdowns/FeaturesDropdown';
|
||||
import MapQuickActions from 'new-dashboard/components/QuickActions/MapQuickActions';
|
||||
import SharedBrief from 'new-dashboard/components/SharedBrief';
|
||||
import methods from './shared/methods';
|
||||
import props from './shared/props';
|
||||
|
||||
export default {
|
||||
name: 'CondensedMapCard',
|
||||
components: {
|
||||
MapQuickActions,
|
||||
FeaturesDropdown,
|
||||
SharedBrief
|
||||
},
|
||||
props,
|
||||
data () {
|
||||
return {
|
||||
...data(),
|
||||
thumbnailWidth: 144,
|
||||
thumbnailHeight: 144
|
||||
};
|
||||
},
|
||||
computed,
|
||||
methods: {
|
||||
...methods,
|
||||
onContentChanged (type) {
|
||||
this.$emit('contentChanged', type);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import 'new-dashboard/styles/variables';
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
padding: 0 14px;
|
||||
background-color: #FFF;
|
||||
|
||||
.cell--thumbnail {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-self: flex-start;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cell__map-thumbnail {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.cell__media {
|
||||
display: flex;
|
||||
position: relative;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
overflow: hidden;
|
||||
transition: all 0.25s cubic-bezier(0.4, 0.01, 0.165, 0.99);
|
||||
border-radius: 2px;
|
||||
background: url($assetsDir + '/images/layout/default-map-bkg.png') no-repeat center 0;
|
||||
background-size: cover;
|
||||
|
||||
&.has-error {
|
||||
.MapCard-error {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cell__checkbox {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
transform: translateY(250%);
|
||||
transition: all 0.25s cubic-bezier(0.4, 0.01, 0.165, 0.99);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $softblue;
|
||||
text-decoration: none;
|
||||
|
||||
&:not(.row--no-hover) {
|
||||
.cell--map-name .cell--map-name__text {
|
||||
color: $primary-color;
|
||||
}
|
||||
|
||||
.cell__title {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.cell__favorite {
|
||||
opacity: 1;
|
||||
|
||||
.favorite-icon {
|
||||
stroke: $text__color--secondary;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.favorite-icon {
|
||||
stroke: $primary-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.metadata__tags-count,
|
||||
.metadata__tag {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.cell__favorite {
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
transform: translate3d(0, 2px, 0);
|
||||
opacity: 0;
|
||||
vertical-align: middle;
|
||||
|
||||
&.is-favorite {
|
||||
opacity: 1;
|
||||
|
||||
.favorite-icon {
|
||||
stroke: #FFC300;
|
||||
fill: #FFC300;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.favorite-icon {
|
||||
stroke: $primary-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cell--last {
|
||||
width: 38px;
|
||||
}
|
||||
|
||||
.quick-actions-placeholder {
|
||||
display: block;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
&.row--selected {
|
||||
box-shadow: 0 0 0 1px $primary-color;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&.row--selected {
|
||||
&.row--can-hover {
|
||||
.cell__media {
|
||||
transform: translateY(-100%);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.cell__checkbox {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
pointer-events: all;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.row--quick-actions-open,
|
||||
&:hover {
|
||||
.quick-actions {
|
||||
.quick-actions__element {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cell__title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&:hover + .metadata .metadata__tag {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
.quick-actions__placeholder {
|
||||
width: 24px;
|
||||
}
|
||||
|
||||
.quick-actions__element {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.metadata {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 4px;
|
||||
|
||||
.metadata__element {
|
||||
margin-left: 16px;
|
||||
|
||||
&:first-of-type {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.metadata__icon,
|
||||
.features-dropdown,
|
||||
ul,
|
||||
li {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.metadata__icon {
|
||||
margin-right: 4px;
|
||||
transform: translate(0, 2px);
|
||||
}
|
||||
|
||||
li {
|
||||
margin-right: 0.2em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,159 @@
|
||||
<template>
|
||||
<div class="map-list-row">
|
||||
<div class="viz-column--main-info">
|
||||
<div class="cell cell--start">
|
||||
</div>
|
||||
<div class="cell cell--main" @click="changeOrder('name')">
|
||||
<span class="text is-small is-txtSoftGrey"
|
||||
:class="{
|
||||
'is-active': isOrderApplied('name'),
|
||||
'is-reversed': isReverseOrderApplied('name'),
|
||||
'element-sort': isSortable
|
||||
}">
|
||||
{{ $t(`MapListHeader.name`) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="viz-column--extra-info">
|
||||
<div class="viz-column--status">
|
||||
<div class="cell cell--large" @click="changeOrder('updated_at')">
|
||||
<span class="text is-small is-txtSoftGrey"
|
||||
:class="{
|
||||
'is-active': isOrderApplied('updated_at'),
|
||||
'is-reversed': isReverseOrderApplied('updated_at'),
|
||||
'element-sort': isSortable
|
||||
}">
|
||||
{{ $t(`MapListHeader.lastModified`) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="cell cell--large u-txt-right u-p--0 " @click="changeOrder('mapviews')">
|
||||
<span class="text is-small is-txtSoftGrey"
|
||||
:class="{
|
||||
'is-active': isOrderApplied('mapviews'),
|
||||
'is-reversed': isReverseOrderApplied('mapviews'),
|
||||
'element-sort': isSortable
|
||||
}">
|
||||
{{ $t(`MapListHeader.views`) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="viz-column--share">
|
||||
<div class="cell cell--small" @click="changeOrder('privacy')">
|
||||
<span class="text is-small is-txtSoftGrey"
|
||||
:class="{
|
||||
'is-active': isOrderApplied('privacy'),
|
||||
'is-reversed': isReverseOrderApplied('privacy'),
|
||||
'element-sort': isSortable
|
||||
}">
|
||||
{{ $t(`MapListHeader.privacy`) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="cell cell--end"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'CondensedMapHeader',
|
||||
props: {
|
||||
order: String,
|
||||
orderDirection: String,
|
||||
isSortable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
changeOrder (order) {
|
||||
if (this.order === order) {
|
||||
return this.setOrder(
|
||||
order,
|
||||
this.getOppositeOrderDirection(this.orderDirection)
|
||||
);
|
||||
}
|
||||
|
||||
this.setOrder(order);
|
||||
},
|
||||
|
||||
getOppositeOrderDirection (order) {
|
||||
if (order === 'desc') {
|
||||
return 'asc';
|
||||
}
|
||||
|
||||
if (order === 'asc') {
|
||||
return 'desc';
|
||||
}
|
||||
},
|
||||
|
||||
isOrderApplied (order) {
|
||||
return order === this.order;
|
||||
},
|
||||
|
||||
isReverseOrderApplied (order) {
|
||||
return order === this.order && this.orderDirection === 'asc';
|
||||
},
|
||||
|
||||
setOrder (order, direction = 'desc') {
|
||||
this.$emit('orderChanged', { order, direction });
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import 'new-dashboard/styles/variables';
|
||||
|
||||
.map-list-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin-bottom: 1px;
|
||||
padding: 14px;
|
||||
overflow: hidden;
|
||||
border-bottom: 1px solid $softblue;
|
||||
background-color: $white;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.cell--start {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-self: flex-start;
|
||||
width: 58px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.cell--end {
|
||||
width: 38px;
|
||||
}
|
||||
|
||||
.element-sort {
|
||||
&.is-active,
|
||||
&:hover {
|
||||
color: $text__color;
|
||||
cursor: pointer;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 14px;
|
||||
height: 100%;
|
||||
margin-left: 4px;
|
||||
transition: all 0.25s cubic-bezier(0.4, 0.01, 0.165, 0.99);
|
||||
background-image: url('../../assets/icons/datasets/chevron.svg');
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
&.is-reversed {
|
||||
&::after {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<component
|
||||
:is="componentType"
|
||||
:visualization="visualization"
|
||||
:isSelected="isSelected"
|
||||
:canHover="canHover"
|
||||
:selectMode="selectMode"
|
||||
:storeActionType="storeActionType"
|
||||
@toggleSelection="toggleSelection"
|
||||
@contentChanged="onContentChanged" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CondensedMapCard from './CondensedMapCard';
|
||||
import SimpleMapCard from './SimpleMapCard';
|
||||
import props from './shared/props';
|
||||
|
||||
export default {
|
||||
name: 'MapCard',
|
||||
props: {
|
||||
...props,
|
||||
condensed: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
components: {
|
||||
SimpleMapCard,
|
||||
CondensedMapCard
|
||||
},
|
||||
computed: {
|
||||
componentType () {
|
||||
return this.condensed ? 'CondensedMapCard' : 'SimpleMapCard';
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleSelection ($event) {
|
||||
this.$emit('toggleSelection', $event);
|
||||
},
|
||||
onContentChanged (type) {
|
||||
this.$emit('contentChanged', type);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,476 @@
|
||||
<template>
|
||||
<a :href="vizUrl"
|
||||
target="_blank"
|
||||
class="card map-card"
|
||||
:class="{
|
||||
'card--selected': isSelected,
|
||||
'card--child-hover': !activeHover,
|
||||
'card--quick-actions-open': areQuickActionsOpen,
|
||||
'card--can-hover': canHover
|
||||
}"
|
||||
@click="onClick">
|
||||
<div class="card-media" :class="{'has-error': isThumbnailErrored}">
|
||||
<img :src="mapThumbnailUrl" @error="onThumbnailError" v-if="isMap && !isThumbnailErrored"/>
|
||||
|
||||
<div class="media-dataset" v-if="!isMap">
|
||||
<img svg-inline src="../../assets/icons/datasets/dataset-icon.svg" />
|
||||
</div>
|
||||
|
||||
<div class="MapCard-error" v-if="isThumbnailErrored"></div>
|
||||
</div>
|
||||
|
||||
<span class="checkbox card-select" @mouseover="mouseOverChildElement" @mouseleave="mouseOutChildElement">
|
||||
<input class="checkbox-input" :class="{'is-selected': isSelected }" @click.stop.prevent="toggleSelection($event)" type="checkbox">
|
||||
<span class="checkbox-decoration">
|
||||
<img svg-inline src="../../assets/icons/common/checkbox.svg">
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<div class="card-actions" v-if="showInteractiveElements" @mouseover="mouseOverChildElement" @mouseleave="mouseOutChildElement">
|
||||
<component
|
||||
:is="quickActionsComponent"
|
||||
|
||||
:map="visualization"
|
||||
:dataset="visualization"
|
||||
:storeActionType="storeActionType"
|
||||
|
||||
@open="openQuickActions"
|
||||
@close="closeQuickActions"
|
||||
@contentChanged="onContentChanged"></component>
|
||||
</div>
|
||||
|
||||
<div class="card-text">
|
||||
<div class="card-header" :class="{ 'card-header__no-description': !sectionsToShow.description}">
|
||||
<h2 :title="visualization.name" class="card-title title is-caption" :class="{'title-overflow': (titleOverflow || isStarInNewLine) && !singleLineTitle, 'single-line': singleLineTitle}">
|
||||
<span :class="{ 'title-element': singleLineTitle }">{{ visualization.name }}</span>
|
||||
<span
|
||||
v-if="showInteractiveElements"
|
||||
class="card-favorite"
|
||||
:class="{'is-favorite': visualization.liked, 'favorite-overflow': titleOverflow}"
|
||||
@click.prevent="toggleFavorite"
|
||||
@mouseover="mouseOverChildElement"
|
||||
@mouseleave="mouseOutChildElement">
|
||||
<img svg-inline src="../../assets/icons/common/favorite.svg">
|
||||
</span>
|
||||
</h2>
|
||||
<template v-if="sectionsToShow.description">
|
||||
<p class="card-description text is-caption" :title="visualization.description" v-if="visualization.description" :class="{'single-line': multilineTitle}">{{ visualization.description }}</p>
|
||||
<p class="card-description text is-caption is-txtSoftGrey" v-else>{{ $t(`MapCard.noDescription`) }}</p>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<ul class="card-metadata">
|
||||
<li class="card-metadataItem text is-caption" v-if="sectionsToShow.privacy && !isSharedWithMe">
|
||||
<span class="icon icon--privacy" :class="privacyIcon"></span>
|
||||
<p>{{ $t(`MapCard.shared.${visualization.privacy}`) }} <span v-if="showViews">| {{ $t(`MapCard.views`, { views: numberViews })}}</span></p>
|
||||
</li>
|
||||
|
||||
<li class="card-metadataItem text is-caption" v-if="sectionsToShow.privacy && isSharedWithMe">
|
||||
<span class="icon icon--privacy icon--sharedBy" :style="{ backgroundImage: `url('${visualization.permission.owner.avatar_url}')` }"></span>
|
||||
<p>{{ $t(`MapCard.sharedBy`, { owner: visualization.permission.owner.username })}}</p>
|
||||
</li>
|
||||
|
||||
<li class="card-metadataItem text is-caption" v-if="sectionsToShow.lastModification">
|
||||
<span class="icon"><img inline-svg src="../../assets/icons/maps/calendar.svg"></span>
|
||||
<p>{{ lastUpdated }}</p>
|
||||
</li>
|
||||
|
||||
<li class="card-metadataItem text is-caption" v-if="sectionsToShow.tags">
|
||||
<span class="icon"><img class="icon__tags" svg-inline src="../../assets/icons/common/tag.svg"></span>
|
||||
|
||||
<ul class="card-tags" v-if="tagsChars <= maxTagsChars">
|
||||
<li v-for="(tag, index) in visualization.tags" :key="tag">
|
||||
<router-link class="card-tags__tag" :to="{ name: 'tagSearch', params: { tag } }" @mouseover="mouseOverChildElement" @mouseleave="mouseOutChildElement">{{ tag }}</router-link><span v-if="index < visualization.tags.length - 1">, </span>
|
||||
</li>
|
||||
|
||||
<li v-if="!tagsLength">
|
||||
<span>{{ $t(`MapCard.noTags`) }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
<FeaturesDropdown v-if="tagsChars > maxTagsChars" :list=visualization.tags linkRoute="tagSearch" feature="tag">
|
||||
<span class="feature-text text is-caption is-txtGrey">{{tagsLength}} {{$t(`MapCard.tags`)}}</span>
|
||||
</FeaturesDropdown>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FeaturesDropdown from 'new-dashboard/components/Dropdowns/FeaturesDropdown';
|
||||
import MapQuickActions from 'new-dashboard/components/QuickActions/MapQuickActions';
|
||||
import DatasetQuickActions from 'new-dashboard/components/QuickActions/DatasetQuickActions';
|
||||
import props from './shared/props';
|
||||
import methods from './shared/methods';
|
||||
import data from './shared/data';
|
||||
import computed from './shared/computed';
|
||||
|
||||
export default {
|
||||
name: 'SimpleMapCard',
|
||||
components: {
|
||||
MapQuickActions,
|
||||
DatasetQuickActions,
|
||||
FeaturesDropdown
|
||||
},
|
||||
props: {
|
||||
...props,
|
||||
visibleSections: Array,
|
||||
singleLineTitle: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
...data(),
|
||||
thumbnailWidth: 600,
|
||||
thumbnailHeight: 280
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...computed,
|
||||
quickActionsComponent () {
|
||||
const visualizationType = this.$props.visualization.type;
|
||||
|
||||
if (visualizationType === 'table') {
|
||||
return 'DatasetQuickActions';
|
||||
}
|
||||
|
||||
if (visualizationType === 'derived') {
|
||||
return 'MapQuickActions';
|
||||
}
|
||||
},
|
||||
sectionsToShow () {
|
||||
const defaultSections = ['description', 'privacy', 'lastModification', 'tags'];
|
||||
const visibleSections = this.$props.visibleSections || defaultSections;
|
||||
|
||||
return visibleSections.reduce((allSections, section) => {
|
||||
allSections[section] = true;
|
||||
return allSections;
|
||||
}, {});
|
||||
},
|
||||
isMap () {
|
||||
return this.$props.visualization.type === 'derived';
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...methods,
|
||||
onContentChanged (type) {
|
||||
this.$emit('contentChanged', type);
|
||||
}
|
||||
},
|
||||
mounted: function () {
|
||||
function isStarUnderText (textNode, starNode) {
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(textNode.firstChild);
|
||||
const textBottomPosition = range.getClientRects()[0].bottom;
|
||||
const starBottomPosition = starNode.getBoundingClientRect().bottom;
|
||||
return textBottomPosition !== starBottomPosition;
|
||||
}
|
||||
|
||||
this.$nextTick(function () {
|
||||
var title = this.$el.querySelector('.card-title');
|
||||
this.multilineTitle = title.offsetHeight > 30;
|
||||
this.titleOverflow = title.scrollHeight > title.clientHeight;
|
||||
this.isStarInNewLine = isStarUnderText(this.$el.querySelector('.card-title'), this.$el.querySelector('.card-favorite'));
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import 'new-dashboard/styles/variables';
|
||||
|
||||
.map-card {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.card {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
transition: box-shadow 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
border: 1px solid $border-color;
|
||||
border-radius: 2px;
|
||||
background-clip: padding-box;
|
||||
background-color: $white;
|
||||
|
||||
&:hover {
|
||||
border-color: transparent;
|
||||
box-shadow: $card__shadow;
|
||||
cursor: pointer;
|
||||
|
||||
&:not(.card--child-hover) {
|
||||
.card-title {
|
||||
color: $primary-color;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
&.card--child-hover {
|
||||
.card-title {
|
||||
color: $text__color;
|
||||
}
|
||||
|
||||
.card-tags {
|
||||
.card-tags__tag {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-actions,
|
||||
.card-favorite {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.card-tags {
|
||||
.card-tags__tag {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.card--selected {
|
||||
border: 1px solid $primary-color;
|
||||
|
||||
.card-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.card--quick-actions-open {
|
||||
.card-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.card--can-hover {
|
||||
&.card--selected,
|
||||
&.card--quick-actions-open,
|
||||
&:hover {
|
||||
.card-select {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-text {
|
||||
padding: 24px 16px;
|
||||
color: $text__color;
|
||||
}
|
||||
|
||||
.card-media {
|
||||
display: flex;
|
||||
position: relative;
|
||||
height: 140px;
|
||||
overflow: hidden;
|
||||
background: url($assetsDir + '/images/layout/default-map-bkg.png') no-repeat center 0;
|
||||
background-size: cover;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
&.has-error {
|
||||
.MapCard-error {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.media-dataset {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: $primary-color;
|
||||
}
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 88px;
|
||||
|
||||
&.card-header__no-description {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.card-title {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
max-height: 48px;
|
||||
margin-bottom: 8px;
|
||||
overflow: hidden;
|
||||
transition: color 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
&.title-overflow {
|
||||
padding-right: 24px;
|
||||
}
|
||||
|
||||
.title-element {
|
||||
max-height: 24px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&.single-line {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.card-description {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
position: relative;
|
||||
max-height: 48px;
|
||||
margin-bottom: 8px;
|
||||
overflow: hidden;
|
||||
|
||||
&.single-line {
|
||||
-webkit-line-clamp: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.card-metadataItem {
|
||||
display: flex;
|
||||
margin-bottom: 8px;
|
||||
|
||||
a {
|
||||
color: $text__color;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: $text__color;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin-top: 4px;
|
||||
margin-right: 8px;
|
||||
|
||||
&.icon--privacy {
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
&.icon--private {
|
||||
background-image: url("../../assets/icons/maps/privacy/lock.svg");
|
||||
}
|
||||
|
||||
&.icon--public {
|
||||
background-image: url("../../assets/icons/maps/privacy/public.svg");
|
||||
}
|
||||
|
||||
&.icon--link {
|
||||
background-image: url("../../assets/icons/maps/privacy/link.svg");
|
||||
}
|
||||
|
||||
&.icon--password {
|
||||
background-image: url("../../assets/icons/maps/privacy/password.svg");
|
||||
}
|
||||
|
||||
&.icon--sharedBy {
|
||||
border-radius: 2px;
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
.icon__tags {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
|
||||
g {
|
||||
fill: $text__color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-select {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
transition: opacity 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
border-radius: 4px;
|
||||
opacity: 0;
|
||||
background: $white;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:checked {
|
||||
opacity: 1;
|
||||
background: $primary-color;
|
||||
}
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
transition: opacity 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.card-favorite {
|
||||
margin-left: 8px;
|
||||
opacity: 0;
|
||||
|
||||
svg {
|
||||
transform: translateY(2px);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.favorite-icon {
|
||||
stroke: $primary-color;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-favorite {
|
||||
opacity: 1;
|
||||
|
||||
.favorite-icon {
|
||||
stroke: #FFC300;
|
||||
fill: #FFC300;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.favorite-icon {
|
||||
stroke: $primary-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.favorite-overflow {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: -4px;
|
||||
}
|
||||
}
|
||||
|
||||
.card-tags > li {
|
||||
display: inline;
|
||||
}
|
||||
</style>
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<div class="condensed-fake-row">
|
||||
<div class="viz-column--main-info">
|
||||
<div class="cell-placeholder cell--icon">
|
||||
<div class="icon-placeholder"/>
|
||||
</div>
|
||||
<div class="cell-placeholder cell--main">
|
||||
<div class="text-placeholder"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="viz-column--extra-info">
|
||||
<div class="viz-column--status u-flex__justify--between">
|
||||
<div class="cell-placeholder cell--large">
|
||||
<div class="text-placeholder"/>
|
||||
</div>
|
||||
<div class="cell-placeholder cell--xsmall">
|
||||
<div class="text-placeholder"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="viz-column--share">
|
||||
<div class="cell-placeholder cell--small">
|
||||
<div class="text-placeholder"/>
|
||||
</div>
|
||||
<div class="cell-placeholder cell--quick-actions">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'CondensedMapCardFake'
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import 'new-dashboard/styles/variables';
|
||||
|
||||
.condensed-fake-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
padding: 0 14px;
|
||||
background-color: $white;
|
||||
}
|
||||
|
||||
.icon-placeholder {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 2px;
|
||||
background-color: $softblue;
|
||||
}
|
||||
|
||||
.text-placeholder {
|
||||
height: 24px;
|
||||
background-color: $softblue;
|
||||
}
|
||||
|
||||
.cell-placeholder {
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.cell--icon {
|
||||
padding: 0 10px 0 0;
|
||||
}
|
||||
|
||||
.cell--quick-actions {
|
||||
padding: 0 18px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<div>
|
||||
<CondensedMapCardFake v-if="condensed"></CondensedMapCardFake>
|
||||
<SimpleMapCardFake v-else></SimpleMapCardFake>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SimpleMapCardFake from 'new-dashboard/components/MapCard/fakes/SimpleMapCardFake';
|
||||
import CondensedMapCardFake from 'new-dashboard/components/MapCard/fakes/CondensedMapCardFake';
|
||||
|
||||
export default {
|
||||
name: 'MapCardFake',
|
||||
components: {
|
||||
SimpleMapCardFake,
|
||||
CondensedMapCardFake
|
||||
},
|
||||
props: {
|
||||
condensed: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<div class="map-fake-card">
|
||||
<div class="image-placeholder"></div>
|
||||
<div class="text-container-placeholder">
|
||||
<div class="text-placeholder"></div>
|
||||
<div class="text-placeholder text-placeholder--double" v-if="sectionsToShow.description"></div>
|
||||
<div class="text-placeholder" v-if="sectionsToShow.privacy"></div>
|
||||
<div class="text-placeholder" v-if="sectionsToShow.lastModification"></div>
|
||||
<div class="text-placeholder" v-if="sectionsToShow.tags"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'SimpleMapCardFake',
|
||||
props: {
|
||||
visibleSections: {
|
||||
type: Array,
|
||||
default: () => ['description', 'privacy', 'lastModification', 'tags']
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
sectionsToShow () {
|
||||
const visibleSections = this.$props.visibleSections || [];
|
||||
|
||||
return visibleSections.reduce((allSections, section) => {
|
||||
allSections[section] = true;
|
||||
return allSections;
|
||||
}, {});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import 'new-dashboard/styles/variables';
|
||||
|
||||
.map-fake-card {
|
||||
border: 1px solid $border-color;
|
||||
border-radius: 2px;
|
||||
background-color: $white;
|
||||
}
|
||||
|
||||
.image-placeholder {
|
||||
width: 100%;
|
||||
height: 140px;
|
||||
background-color: $softblue;
|
||||
}
|
||||
|
||||
.text-container-placeholder {
|
||||
padding: 24px 16px;
|
||||
}
|
||||
|
||||
.text-placeholder {
|
||||
width: 100%;
|
||||
height: 24px;
|
||||
margin-bottom: 8px;
|
||||
background-color: $softblue;
|
||||
|
||||
&.text-placeholder--double {
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,55 @@
|
||||
import * as Visualization from 'new-dashboard/core/models/visualization';
|
||||
import countCharsArray from 'new-dashboard/utils/count-chars-array';
|
||||
import distanceInWordsStrict from 'date-fns/distance_in_words_strict';
|
||||
import zhCN from 'date-fns/locale/zh_cn';
|
||||
|
||||
|
||||
export default {
|
||||
privacyIcon () {
|
||||
return `icon--${this.$props.visualization.privacy}`.toLowerCase();
|
||||
},
|
||||
lastUpdated () {
|
||||
return this.$t(`MapCard.lastUpdate`, {
|
||||
date: distanceInWordsStrict(this.$props.visualization.updated_at, new Date(),{ locale: zhCN })
|
||||
});
|
||||
},
|
||||
mapThumbnailUrl () {
|
||||
return Visualization.getThumbnailUrl(this.$props.visualization, this.$cartoModels, {
|
||||
width: this.thumbnailWidth,
|
||||
height: this.thumbnailHeight
|
||||
});
|
||||
},
|
||||
tagsLength () {
|
||||
return this.$props.visualization.tags ? this.$props.visualization.tags.length : 0;
|
||||
},
|
||||
hasTags () {
|
||||
return this.$props.visualization.tags && this.$props.visualization.tags.length;
|
||||
},
|
||||
vizUrl () {
|
||||
return Visualization.getURL(this.$props.visualization, this.$cartoModels);
|
||||
},
|
||||
tagsChars () {
|
||||
return countCharsArray(this.$props.visualization.tags, ', ');
|
||||
},
|
||||
isSharedWithMe () {
|
||||
return Visualization.isSharedWithMe(this.$props.visualization, this.$cartoModels);
|
||||
},
|
||||
showViews () {
|
||||
const privacy = this.$props.visualization.privacy;
|
||||
return ['public', 'password', 'link'].includes(privacy.toLowerCase());
|
||||
},
|
||||
numberViews () {
|
||||
const stats = this.$props.visualization.stats;
|
||||
const totalViews = Object.keys(stats).reduce((total, date) => total + stats[date], 0);
|
||||
return totalViews;
|
||||
},
|
||||
showInteractiveElements () {
|
||||
return !this.$props.selectMode;
|
||||
},
|
||||
colleaguesSharedList () {
|
||||
return this.$props.visualization.permission.acl;
|
||||
},
|
||||
isSharedWithColleagues () {
|
||||
return this.$props.visualization.permission.acl.length > 0;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
export default function data () {
|
||||
return {
|
||||
isThumbnailErrored: false,
|
||||
activeHover: true,
|
||||
titleOverflow: false,
|
||||
multilineTitle: false,
|
||||
areQuickActionsOpen: false,
|
||||
isStarInNewLine: false,
|
||||
maxTagsChars: 30
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
export default {
|
||||
toggleSelection ($event) {
|
||||
this.$emit('toggleSelection', {
|
||||
map: this.$props.visualization,
|
||||
isSelected: !this.$props.isSelected,
|
||||
event: $event
|
||||
});
|
||||
},
|
||||
toggleFavorite () {
|
||||
if (this.$props.visualization.liked) {
|
||||
this.$store.dispatch(`${this.$props.storeActionType}/deleteLike`, this.$props.visualization);
|
||||
} else {
|
||||
this.$store.dispatch(`${this.$props.storeActionType}/like`, this.$props.visualization);
|
||||
}
|
||||
},
|
||||
openQuickActions () {
|
||||
this.areQuickActionsOpen = true;
|
||||
},
|
||||
closeQuickActions () {
|
||||
this.areQuickActionsOpen = false;
|
||||
},
|
||||
mouseOverChildElement () {
|
||||
this.activeHover = false;
|
||||
},
|
||||
mouseOutChildElement () {
|
||||
this.activeHover = true;
|
||||
},
|
||||
onThumbnailError () {
|
||||
this.isThumbnailErrored = true;
|
||||
},
|
||||
onDataChanged () {
|
||||
this.$emit('dataChanged');
|
||||
},
|
||||
onClick (event) {
|
||||
if (this.$props.selectMode) {
|
||||
event.preventDefault();
|
||||
this.toggleSelection(event);
|
||||
}
|
||||
},
|
||||
isLastTag (index) {
|
||||
return index === this.tagsLength - 1;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
export default {
|
||||
visualization: Object,
|
||||
storeActionType: {
|
||||
type: String,
|
||||
default: 'maps'
|
||||
},
|
||||
isSelected: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
canHover: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
selectMode: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,363 @@
|
||||
<template>
|
||||
<div class="container grid">
|
||||
<div class="full-width">
|
||||
<SectionTitle class="grid-cell" :showActionButton="!selectedMaps.length" ref="headerContainer">
|
||||
<template slot="icon">
|
||||
<img src="../assets/icons/section-title/map.svg">
|
||||
</template>
|
||||
|
||||
<template slot="title">
|
||||
<VisualizationsTitle
|
||||
:defaultTitle="$t(`MapsPage.header.title['${appliedFilter}']`)"
|
||||
:selectedItems="selectedMaps.length"
|
||||
:vizQuota="publicMapsQuota"
|
||||
:vizCount="publicMapsCount"
|
||||
:isOutOfQuota="isOutOfPublicMapsQuota"
|
||||
:counterLabel="'Public Maps'"/>
|
||||
</template>
|
||||
|
||||
<template v-if="shouldShowLimitsWarning" slot="warning">
|
||||
<BadgeWarning>
|
||||
<div class="warning" v-html="$t('MapsPage.header.warning', { counter: `${publicMapsCount}/${publicMapsQuota}`, path: upgradeUrl })"></div>
|
||||
</BadgeWarning>
|
||||
</template>
|
||||
|
||||
<template slot="dropdownButton">
|
||||
<MapBulkActions
|
||||
:selectedMaps="selectedMaps"
|
||||
:areAllMapsSelected="areAllMapsSelected"
|
||||
v-if="hasBulkActions && selectedMaps.length"
|
||||
@selectAll="selectAll"
|
||||
@deselectAll="deselectAll"></MapBulkActions>
|
||||
|
||||
<SettingsDropdown
|
||||
section="maps"
|
||||
v-if="!selectedMaps.length"
|
||||
:filter="appliedFilter"
|
||||
:order="appliedOrder"
|
||||
:orderDirection="appliedOrderDirection"
|
||||
:metadata="mapsMetadata"
|
||||
@filterChanged="applyFilter">
|
||||
<img svg-inline src="../assets/icons/common/filter.svg">
|
||||
</SettingsDropdown>
|
||||
|
||||
<div class="mapcard-view-mode" @click="toggleViewMode" v-if="shouldShowViewSwitcher">
|
||||
<img svg-inline src="../assets/icons/common/compactMap.svg" v-if="!isCondensed">
|
||||
<img svg-inline src="../assets/icons/common/standardMap.svg" v-if="isCondensed">
|
||||
</div>
|
||||
</template>
|
||||
<template slot="actionButton" v-if="!isFirstTimeViewingDashboard && !selectedMaps.length">
|
||||
<CreateButton visualizationType="maps" :disabled="isViewer">{{ $t(`MapsPage.createMap`) }}</CreateButton>
|
||||
</template>
|
||||
</SectionTitle>
|
||||
|
||||
<div class="grid-cell" v-if="initialState">
|
||||
<CreateMapCard></CreateMapCard>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="shouldShowListHeader"
|
||||
class="grid-cell grid-cell--noMargin grid-cell--col12 grid__head--sticky"
|
||||
:class="{ 'has-user-notification': isNotificationVisible }">
|
||||
<CondensedMapHeader
|
||||
:order="appliedOrder"
|
||||
:orderDirection="appliedOrderDirection"
|
||||
@orderChanged="applyOrder"
|
||||
v-if="shouldShowListHeader">
|
||||
</CondensedMapHeader>
|
||||
</div>
|
||||
|
||||
<ul :class="[isCondensed ? 'grid grid-cell' : 'grid']" v-if="isFetchingMaps">
|
||||
<li :class="[isCondensed ? condensedCSSClasses : cardCSSClasses]" v-for="n in maxVisibleMaps" :key="n">
|
||||
<MapCardFake :condensed="isCondensed"></MapCardFake>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ul :class="[isCondensed ? 'grid grid-column grid-cell' : 'grid']" v-if="!isFetchingMaps && currentEntriesCount > 0">
|
||||
<li v-for="map in maps" :class="[isCondensed ? condensedCSSClasses : cardCSSClasses]" :key="map.id">
|
||||
<MapCard
|
||||
:condensed="isCondensed"
|
||||
:visualization="map"
|
||||
:isSelected="isMapSelected(map)"
|
||||
:selectMode="isSomeMapSelected"
|
||||
:canHover="canHoverCard"
|
||||
@toggleSelection="toggleSelected"
|
||||
@contentChanged="onContentChanged">
|
||||
</MapCard>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<EmptyState
|
||||
:text="emptyStateText"
|
||||
v-if="emptyState">
|
||||
<img svg-inline src="../assets/icons/common/compass.svg">
|
||||
</EmptyState>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapGetters } from 'vuex';
|
||||
import CreateButton from 'new-dashboard/components/CreateButton.vue';
|
||||
import CreateMapCard from 'new-dashboard/components/CreateMapCard';
|
||||
import EmptyState from 'new-dashboard/components/States/EmptyState';
|
||||
import InitialState from 'new-dashboard/components/States/InitialState';
|
||||
import MapBulkActions from 'new-dashboard/components/BulkActions/MapBulkActions.vue';
|
||||
import MapCard from 'new-dashboard/components/MapCard/MapCard.vue';
|
||||
import CondensedMapHeader from 'new-dashboard/components/MapCard/CondensedMapHeader.vue';
|
||||
import MapCardFake from 'new-dashboard/components/MapCard/fakes/MapCardFake';
|
||||
import SectionTitle from 'new-dashboard/components/SectionTitle';
|
||||
import VisualizationsTitle from 'new-dashboard/components/VisualizationsTitle';
|
||||
import BadgeWarning from 'new-dashboard/components/BadgeWarning';
|
||||
import SettingsDropdown from 'new-dashboard/components/Settings/Settings';
|
||||
import { shiftClick } from 'new-dashboard/utils/shift-click.service.js';
|
||||
|
||||
export default {
|
||||
name: 'MapsList',
|
||||
props: {
|
||||
maxVisibleMaps: {
|
||||
type: Number,
|
||||
default: 12
|
||||
},
|
||||
hasBulkActions: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
canHoverCard: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
canChangeViewMode: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
isCondensedDefault: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
components: {
|
||||
CreateButton,
|
||||
CreateMapCard,
|
||||
EmptyState,
|
||||
SettingsDropdown,
|
||||
MapBulkActions,
|
||||
MapCard,
|
||||
CondensedMapHeader,
|
||||
MapCardFake,
|
||||
SectionTitle,
|
||||
VisualizationsTitle,
|
||||
BadgeWarning,
|
||||
InitialState
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
selectedMaps: [],
|
||||
cardCSSClasses: 'grid-cell grid-cell--col4 grid-cell--col6--tablet grid-cell--col12--mobile map-element',
|
||||
condensedCSSClasses: 'card-condensed',
|
||||
isCondensed: this.isCondensedDefault,
|
||||
lastCheckedItem: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
appliedFilter: state => state.maps.filterType,
|
||||
appliedOrder: state => state.maps.order,
|
||||
appliedOrderDirection: state => state.maps.orderDirection,
|
||||
maps: state => state.maps.list,
|
||||
mapsMetadata: state => state.maps.metadata,
|
||||
isFetchingMaps: state => state.maps.isFetching,
|
||||
currentEntriesCount: state => state.maps.metadata.total_entries,
|
||||
filterType: state => state.maps.filterType,
|
||||
totalUserEntries: state => state.maps.metadata.total_user_entries,
|
||||
totalShared: state => state.maps.metadata.total_shared,
|
||||
isFirstTimeViewingDashboard: state => state.config.isFirstTimeViewingDashboard,
|
||||
upgradeUrl: state => state.config.upgrade_url
|
||||
}),
|
||||
...mapGetters({
|
||||
publicMapsQuota: 'user/publicMapsQuota',
|
||||
publicMapsCount: 'user/publicMapsCount',
|
||||
isOutOfPublicMapsQuota: 'user/isOutOfPublicMapsQuota'
|
||||
}),
|
||||
areAllMapsSelected () {
|
||||
return Object.keys(this.maps).length === this.selectedMaps.length;
|
||||
},
|
||||
initialState () {
|
||||
return this.isFirstTimeViewingDashboard &&
|
||||
!this.hasSharedMaps &&
|
||||
!this.isFetchingMaps &&
|
||||
this.hasFilterApplied('mine') &&
|
||||
this.totalUserEntries <= 0;
|
||||
},
|
||||
emptyState () {
|
||||
return ((!this.isFirstTimeViewingDashboard || this.hasSharedMaps) || this.isFirstTimeViewerAfterAction) &&
|
||||
!this.isFetchingMaps &&
|
||||
!this.currentEntriesCount;
|
||||
},
|
||||
emptyStateText () {
|
||||
const route = this.$router.resolve({name: 'maps', params: { filter: 'shared' }});
|
||||
|
||||
return this.hasSharedMaps
|
||||
? this.$t('MapsPage.emptyCase.onlyShared', { path: route.href })
|
||||
: this.$t('MapsPage.emptyCase.default', { path: route.href });
|
||||
},
|
||||
isFirstTimeViewerAfterAction () {
|
||||
// First time viewing dashboard but user has performed any action such as drag and dropping a dataset (no page refreshing)
|
||||
return this.isFirstTimeViewingDashboard && this.currentEntriesCount <= 0 && !this.hasFilterApplied('mine');
|
||||
},
|
||||
hasSharedMaps () {
|
||||
return this.totalShared > 0;
|
||||
},
|
||||
isSomeMapSelected () {
|
||||
return this.selectedMaps.length > 0;
|
||||
},
|
||||
shouldShowLimitsWarning () {
|
||||
return !this.selectedMaps.length && this.isOutOfPublicMapsQuota;
|
||||
},
|
||||
shouldShowViewSwitcher () {
|
||||
return this.canChangeViewMode && !this.initialState && !this.emptyState && !this.selectedMaps.length;
|
||||
},
|
||||
shouldShowListHeader () {
|
||||
return this.isCondensed && !this.emptyState && !this.initialState;
|
||||
},
|
||||
isViewer () {
|
||||
return this.$store.getters['user/isViewer'];
|
||||
},
|
||||
isNotificationVisible () {
|
||||
return this.$store.getters['user/isNotificationVisible'];
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
fetchMaps () {
|
||||
this.$store.dispatch('maps/fetch');
|
||||
},
|
||||
applyFilter (filter) {
|
||||
this.$emit('applyFilter', filter);
|
||||
},
|
||||
applyOrder (orderParams) {
|
||||
this.$emit('applyOrder', orderParams);
|
||||
},
|
||||
toggleSelected ({ map, isSelected, event }) {
|
||||
if (this.selectedMaps.length && event.shiftKey) {
|
||||
this.doShiftClick(map);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSelected) {
|
||||
this.lastCheckedItem = map;
|
||||
this.selectedMaps.push(map);
|
||||
return;
|
||||
}
|
||||
|
||||
this.selectedMaps = this.selectedMaps.filter(selectedMap => selectedMap.id !== map.id);
|
||||
},
|
||||
doShiftClick (map) {
|
||||
const mapsArray = [...Object.values(this.maps)];
|
||||
this.selectedMaps = shiftClick(mapsArray, this.selectedMaps, map, this.lastCheckedItem || map);
|
||||
},
|
||||
selectAll () {
|
||||
this.selectedMaps = [...Object.values(this.$store.state.maps.list)];
|
||||
},
|
||||
deselectAll () {
|
||||
this.selectedMaps = [];
|
||||
},
|
||||
isMapSelected (map) {
|
||||
return this.selectedMaps.some(selectedMap => selectedMap.id === map.id);
|
||||
},
|
||||
hasFilterApplied (filter) {
|
||||
return this.filterType === filter;
|
||||
},
|
||||
toggleViewMode () {
|
||||
this.isCondensed = !this.isCondensed;
|
||||
},
|
||||
getHeaderContainer () {
|
||||
return this.$refs.headerContainer;
|
||||
},
|
||||
onContentChanged (type) {
|
||||
this.$emit('contentChanged', type);
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
isCondensed (isCompactMapView) {
|
||||
if (isCompactMapView) {
|
||||
localStorage.mapViewMode = 'compact';
|
||||
} else {
|
||||
localStorage.mapViewMode = 'standard';
|
||||
}
|
||||
},
|
||||
selectedMaps () {
|
||||
this.$emit('selectionChange', this.selectedMaps);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import 'new-dashboard/styles/variables';
|
||||
|
||||
.map-element {
|
||||
margin-bottom: 36px;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.grid__head--sticky {
|
||||
top: $header__height;
|
||||
}
|
||||
|
||||
.grid__head--sticky.has-user-notification {
|
||||
top: $header__height + $notification-warning__height;
|
||||
}
|
||||
|
||||
.pagination-element {
|
||||
margin-top: 28px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
margin: 20vh 0 8vh;
|
||||
}
|
||||
|
||||
.grid-column {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.card-condensed {
|
||||
width: 100%;
|
||||
border-bottom: 1px solid #EBEEF5;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.mapcard-view-mode {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 38px;
|
||||
height: 36px;
|
||||
margin-left: 24px;
|
||||
padding: 9px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: $softblue;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: $primary-color;
|
||||
|
||||
.svgicon {
|
||||
fill: $white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.warning {
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<modal :name="name" width="100%" height="100%" @closed="close">
|
||||
<button class="modal__button modal__button--close" @click="close" aria-label="Close"></button>
|
||||
<slot />
|
||||
</modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import VModal from 'vue-js-modal';
|
||||
|
||||
export default {
|
||||
name: 'Wizard',
|
||||
components: {
|
||||
VModal
|
||||
},
|
||||
mounted () {
|
||||
this.setup();
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.close();
|
||||
},
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
default: 'modal'
|
||||
},
|
||||
isOpen: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setup () {
|
||||
if (this.isOpen) {
|
||||
this.open();
|
||||
} else {
|
||||
this.close();
|
||||
}
|
||||
},
|
||||
open () {
|
||||
this.$modal.show(this.name);
|
||||
document.body.classList.add('u-overflow-hidden');
|
||||
},
|
||||
close () {
|
||||
document.body.classList.remove('u-overflow-hidden');
|
||||
this.$modal.hide(this.name);
|
||||
this.$emit('closeModal');
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
isOpen () {
|
||||
this.setup();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import 'new-dashboard/styles/variables';
|
||||
|
||||
.modal {
|
||||
&__button {
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
padding: 2em;
|
||||
|
||||
&--close {
|
||||
right: 0;
|
||||
background-image: url('../assets/icons/common/close.svg');
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,284 @@
|
||||
<template>
|
||||
<nav class="navbar" :class="{ 'is-search-open': isSearchOpen, 'has-user-notification': isNotificationVisible }">
|
||||
<ul class="navbar-elementsContainer">
|
||||
<router-link :to="{ name: 'home' }" class="navbar-elementItem" :class="{'is-active': isHomePage()}" staticRoute="/dashboard">
|
||||
<span class="navbar-icon">
|
||||
<img svg-inline class="navbar-iconFill" src="../../assets/icons/navbar/home.svg" />
|
||||
</span>
|
||||
<span class="title is-caption is-txtWhite u-hideTablet">主页</span>
|
||||
</router-link>
|
||||
<router-link :to="{ name: 'maps' }" class="navbar-elementItem" active-class="is-active" staticRoute="/dashboard/maps">
|
||||
<span class="navbar-icon">
|
||||
<img svg-inline class="navbar-iconFill" src="../../assets/icons/navbar/maps.svg" />
|
||||
</span>
|
||||
<span class="title is-caption is-txtWhite u-hideTablet">地图</span>
|
||||
</router-link>
|
||||
<!-- <router-link :to="{ name: 'solutions' }" class="navbar-elementItem" active-class="is-active">
|
||||
<span class="navbar-icon">
|
||||
<img svg-inline class="navbar-iconFill" src="../../assets/icons/navbar/solutions.svg" />
|
||||
</span>
|
||||
<span class="title is-caption is-txtWhite u-hideTablet">Solutions</span>
|
||||
</router-link> -->
|
||||
<router-link :to="{ name: 'datasets' }" class="navbar-elementItem" active-class="is-active" staticRoute="/dashboard/datasets">
|
||||
<span class="navbar-icon">
|
||||
<img svg-inline class="navbar-iconFill" src="../../assets/icons/navbar/data.svg" />
|
||||
</span>
|
||||
<span class="title is-caption is-txtWhite u-hideTablet">数据</span>
|
||||
</router-link>
|
||||
<span class="navbar-elementItem navbar-icon navbar-searchIcon u-showMobile" @click="toggleSearch">
|
||||
<img svg-inline src="../../assets/icons/navbar/loupe_white.svg" />
|
||||
</span>
|
||||
</ul>
|
||||
<div class="navbar-imagotype u-hideMobile">
|
||||
<img src="../../assets/icons/navbar/imagotype.svg" />
|
||||
</div>
|
||||
<div class="navbar-elementsContainer navbar-searchContainer">
|
||||
<Search />
|
||||
<div class="navbar-user">
|
||||
<div class="navbar-avatar" :class="{'has-notification': notificationsCount}" :style="{ backgroundImage: `url('${user.avatar_url}')` }" @click.stop.prevent="toggleDropdown"></div>
|
||||
<UserDropdown :userModel="user" :notificationsCount="notificationsCount" :open="isDropdownOpen" :baseUrl="baseUrl" v-click-outside="closeDropdown" @linkClick="closeDropdown" />
|
||||
</div>
|
||||
<span class="navbar-searchClose" @click="toggleSearch">
|
||||
<img svg-inline src="../../assets/icons/navbar/close.svg" />
|
||||
</span>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Search from '../Search/Search';
|
||||
import UserDropdown from './UserDropdown';
|
||||
import FeedbackPopup from '../FeedbackPopup';
|
||||
import storageAvailable from 'new-dashboard/utils/is-storage-available';
|
||||
|
||||
export default {
|
||||
name: 'NavigationBar',
|
||||
components: {
|
||||
Search,
|
||||
UserDropdown,
|
||||
FeedbackPopup
|
||||
},
|
||||
props: {
|
||||
user: Object,
|
||||
baseUrl: String,
|
||||
notificationsCount: Number,
|
||||
isNotificationVisible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
isFirstTimeInDashboard: Boolean,
|
||||
bundleType: {
|
||||
type: String,
|
||||
default: 'other'
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
isDropdownOpen: false,
|
||||
isSearchOpen: false,
|
||||
hasDropdownOpenedForFirstTime: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isDashboardBundle () {
|
||||
return this.$props.bundleType === 'dashboard';
|
||||
},
|
||||
popupWasShown () {
|
||||
if (!storageAvailable('localStorage')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return JSON.parse(window.localStorage.getItem('carto.feedback.popupWasShown'));
|
||||
},
|
||||
shouldShowFeedbackPopup () {
|
||||
return this.isDashboardBundle &&
|
||||
!this.isFirstTimeInDashboard &&
|
||||
!this.hasDropdownOpenedForFirstTime &&
|
||||
!this.popupWasShown;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleDropdown () {
|
||||
this.hasDropdownOpenedForFirstTime = true;
|
||||
this.isDropdownOpen = !this.isDropdownOpen;
|
||||
},
|
||||
closeDropdown () {
|
||||
this.isDropdownOpen = false;
|
||||
},
|
||||
toggleSearch () {
|
||||
this.isSearchOpen = !this.isSearchOpen;
|
||||
},
|
||||
isHomePage () {
|
||||
return (this.$route || {}).name === 'home';
|
||||
},
|
||||
onFeedbackClicked () {
|
||||
this.toggleDropdown();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import 'new-dashboard/styles/variables';
|
||||
|
||||
.navbar {
|
||||
display: flex;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
z-index: $z-index__navbar;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: 0 64px;
|
||||
border-bottom: 1px solid $white;
|
||||
background-color: $primary-color;
|
||||
|
||||
@media (max-width: $layout-tablet) {
|
||||
padding: 0 24px 0 40px;
|
||||
}
|
||||
|
||||
@media (max-width: $layout-mobile) {
|
||||
padding: 0 16px 0 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-elementsContainer {
|
||||
display: flex;
|
||||
position: relative;
|
||||
|
||||
a:last-of-type {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
@media (max-width: $layout-mobile) {
|
||||
a:last-of-type {
|
||||
margin-right: 36px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-elementItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 54px;
|
||||
padding: 20px 2px 16px;
|
||||
border-bottom: 4px solid transparent;
|
||||
|
||||
@media (max-width: $layout-tablet) {
|
||||
margin-right: 48px;
|
||||
}
|
||||
|
||||
@media (max-width: $layout-mobile) {
|
||||
margin-right: 36px;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
border-color: $white;
|
||||
|
||||
.navbar-iconFill {
|
||||
fill: #FFF;
|
||||
}
|
||||
|
||||
.navbar-iconNegativeFill {
|
||||
fill: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-searchIcon {
|
||||
padding-top: 21px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.navbar-searchClose {
|
||||
display: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.navbar-icon {
|
||||
margin-right: 12px;
|
||||
|
||||
@media (max-width: $layout-tablet) {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-imagotype {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
transform: translate3d(-50%, 0, 0);
|
||||
}
|
||||
|
||||
.navbar-user {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.navbar-avatar {
|
||||
display: flex;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
margin-left: 24px;
|
||||
overflow: hidden;
|
||||
border-radius: 50%;
|
||||
background-color: $navbar-avatar__bg-color;
|
||||
background-size: cover;
|
||||
|
||||
&.has-notification {
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
right: -4px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border: 2px solid $primary-color;
|
||||
border-radius: 50%;
|
||||
background-color: $notification__bg-color;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar.is-search-open {
|
||||
padding: 0;
|
||||
|
||||
.navbar-elementsContainer {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.navbar-elementsContainer,
|
||||
.navbar-imagotype,
|
||||
.navbar-user {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.navbar-searchContainer {
|
||||
visibility: visible;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.navbar-searchClose {
|
||||
display: block;
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar.has-user-notification {
|
||||
top: $notification-warning__height;
|
||||
}
|
||||
|
||||
.feedback-popup {
|
||||
position: absolute;
|
||||
top: calc(100% + 18px);
|
||||
right: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,202 @@
|
||||
<template>
|
||||
<div class="navbar-dropdown" :class="{ 'is-open': open }">
|
||||
<ul class="navbar-dropdown-container">
|
||||
<li class="navbar-dropdown-profile">
|
||||
<div class="avatar-container">
|
||||
<img :src="userModel.avatar_url">
|
||||
</div>
|
||||
<div class="navbar-dropdown-userInfo">
|
||||
<p class="text is-semibold is-caption">{{userModel.username}}</p>
|
||||
<p class="text is-small">{{userModel.email}}</p>
|
||||
</div>
|
||||
</li>
|
||||
<li class="navbar-dropdown-iconLink">
|
||||
<div class="icon-container">
|
||||
<img svg-inline src="../../assets/icons/navbar/dropdown/settings.svg"/>
|
||||
</div>
|
||||
<a :href="`${ baseUrl }/profile`" class="text is-semibold is-caption is-txtGrey" @click="linkClicked">{{ $t('UserDropdown.settings') }}</a>
|
||||
</li>
|
||||
<li class="navbar-dropdown-iconLink" v-if="isOrganizationAdmin">
|
||||
<div class="icon-container">
|
||||
<img svg-inline src="../../assets/icons/navbar/dropdown/organization.svg"/>
|
||||
</div>
|
||||
<a :href="`${ baseUrl }/organization`" class="text is-semibold is-caption is-txtGrey" @click="linkClicked">{{ $t('UserDropdown.organizationSettings') }}</a>
|
||||
</li>
|
||||
<li @click="linkClicked" class="navbar-dropdown-iconLink">
|
||||
<div class="icon-container">
|
||||
<img svg-inline src="../../assets/icons/navbar/dropdown/api-keys.svg"/>
|
||||
</div>
|
||||
<a :href="`${ baseUrl }/your_apps`" class="text is-semibold is-caption is-txtGrey" @click="linkClicked">{{ $t('UserDropdown.apiKeys') }}</a>
|
||||
</li>
|
||||
<li class="navbar-dropdown-iconLink">
|
||||
<div class="icon-container">
|
||||
<img svg-inline src="../../assets/icons/navbar/dropdown/profile.svg"/>
|
||||
</div>
|
||||
<a :href="`${ baseUrl }/me`" class="text is-semibold is-caption is-txtGrey" target="_blank" @click="linkClicked">{{ $t('UserDropdown.publicProfile') }}</a>
|
||||
</li>
|
||||
<li class="navbar-dropdown-iconLink">
|
||||
<div class="icon-container">
|
||||
<img svg-inline src="../../assets/icons/navbar/dropdown/notifications.svg"/>
|
||||
</div>
|
||||
<router-link :to="{ name: 'notifications' }" class="text is-semibold is-caption is-txtGrey" @click.native="linkClicked" :staticRoute="'/dashboard/notifications'">
|
||||
{{ $t('UserDropdown.notifications') }}
|
||||
</router-link>
|
||||
<span v-if="notificationsCount > 0" class="notification-number text is-semibold is-small is-txtGrey">{{notificationsCount}}</span>
|
||||
</li>
|
||||
<!--
|
||||
<li class="navbar-dropdown-iconLink">
|
||||
<div class="icon-container">
|
||||
<img svg-inline src="../../assets/icons/navbar/dropdown/feedback.svg"/>
|
||||
</div>
|
||||
<a href="https://docs.google.com/forms/d/e/1FAIpQLScBQUWd-TP3Qy514DOCNg-KoLrViHijUR5giLAMS-3jmDnrPg/viewform" class="text is-semibold is-caption is-txtGrey" target="_blank" @click="linkClicked">
|
||||
{{ $t('UserDropdown.feedback') }}
|
||||
</a>
|
||||
</li>
|
||||
-->
|
||||
</ul>
|
||||
<a :href="`${ baseUrl }/logout`" class="navbar-dropdown-bottom" @click="linkClicked">
|
||||
<div class="arrow"><img src="../../assets/icons/navbar/dropdown/arrow-right.svg" /></div>
|
||||
<div class="title is-small is-txtAlert">{{ $t('UserDropdown.logout') }}</div>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'UserDropdown',
|
||||
props: {
|
||||
baseUrl: { type: String, default: '' },
|
||||
open: { type: Boolean, default: false },
|
||||
userModel: Object,
|
||||
notificationsCount: Number
|
||||
},
|
||||
computed: {
|
||||
isOrganizationAdmin () {
|
||||
return this.userModel.org_admin;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
linkClicked () {
|
||||
this.$emit('linkClick');
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
@import 'new-dashboard/styles/variables';
|
||||
|
||||
.navbar-dropdown {
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
width: 360px;
|
||||
margin-top: 18px;
|
||||
transition: all 0.25s linear;
|
||||
border-radius: 4px;
|
||||
opacity: 0;
|
||||
box-shadow: $dropdown__shadow;
|
||||
pointer-events: none;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
transform: rotate(45deg) translateX(-50%);
|
||||
transform-origin: center center;
|
||||
border-radius: 4px;
|
||||
background-color: $white;
|
||||
box-shadow: $dropdown__shadow;
|
||||
}
|
||||
|
||||
&.is-open {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
pointer-events: initial;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-dropdown-profile {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 36px;
|
||||
|
||||
.avatar-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
margin-right: 12px;
|
||||
overflow: hidden;
|
||||
border-radius: 50%;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-dropdown-container {
|
||||
position: relative;
|
||||
padding: 28px 24px 36px;
|
||||
border-radius: 4px 4px 0 0;
|
||||
background-color: $white;
|
||||
}
|
||||
|
||||
.navbar-dropdown-iconLink {
|
||||
display: flex;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.icon-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-dropdown-bottom {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
padding: 24px 30px;
|
||||
border-radius: 0 0 4px 4px;
|
||||
background-color: $softblue;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
& .arrow {
|
||||
margin-right: 8px;
|
||||
|
||||
img {
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.notification-number {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-top: 2px;
|
||||
margin-left: 24px;
|
||||
border-radius: 50%;
|
||||
background-color: $notification__bg-color;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<div class="notification">
|
||||
<div class="notification-info">
|
||||
<div class="notification-received text is-small">{{ receivedAtFormatted }}</div>
|
||||
<span v-if="!readAt" class="notification-read"></span>
|
||||
</div>
|
||||
<div v-html="htmlBody" class="notification-html text is-caption" :class="{'is-unread': !readAt}"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'NotificationCard',
|
||||
props: {
|
||||
id: String,
|
||||
receivedAt: String,
|
||||
readAt: String,
|
||||
htmlBody: String
|
||||
},
|
||||
computed: {
|
||||
receivedAtFormatted () {
|
||||
return new Date(this.receivedAt).toLocaleDateString();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
@import 'new-dashboard/styles/variables';
|
||||
|
||||
.notification-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.notification-received {
|
||||
color: $text__color--secondary;
|
||||
}
|
||||
|
||||
.notification-read {
|
||||
display: block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin-left: 24px;
|
||||
border-radius: 50%;
|
||||
background: $notification__bg-color;
|
||||
}
|
||||
|
||||
.notification-html {
|
||||
margin-top: 16px;
|
||||
color: $text__color;
|
||||
|
||||
&.is-unread {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<div class="notification">
|
||||
<span v-html="htmlBody" class="text notification__text"></span>
|
||||
<button class="notification__close-button" @click="close" aria-label="Close">
|
||||
<img src="../assets/icons/common/close.svg" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'NotificationWarning',
|
||||
props: {
|
||||
htmlBody: String
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.$store && this.$store.dispatch('user/hideUserNotification');
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
@import 'new-dashboard/styles/variables';
|
||||
|
||||
.notification {
|
||||
display: flex;
|
||||
position: fixed;
|
||||
z-index: $z-index__notification-warning;
|
||||
top: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: $notification-warning__height;
|
||||
max-height: $notification-warning__height;
|
||||
padding: 16px;
|
||||
background-color: $notification__bg-color;
|
||||
color: $text__color;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
|
||||
.notification__text {
|
||||
display: inline-block;
|
||||
width: 80%;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
/deep/ b {
|
||||
font-weight: 900;
|
||||
}
|
||||
}
|
||||
|
||||
.notification__close-button {
|
||||
display: none; // TO DO: improve notification warning behaviour
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 24px;
|
||||
height: 100%;
|
||||
|
||||
img {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<button
|
||||
class="button"
|
||||
:class="[{'not-visited': !(hasVisitedOnboarding || isFirstTimeViewingDashboard)}, isFirstTimeViewingDashboard ? 'button--cta' : 'button--ghost']"
|
||||
@click="openOnboarding()">
|
||||
{{ $t('Wizards.Distributor.cta') }}
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'OnboardingButton',
|
||||
props: {
|
||||
isFirstTimeViewingDashboard: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
hasVisitedOnboarding: false
|
||||
};
|
||||
},
|
||||
mounted () {
|
||||
this.hasVisitedOnboarding = this.setHasVisitedOnboarding();
|
||||
},
|
||||
methods: {
|
||||
openOnboarding () {
|
||||
this.updateHasVisitedOnboarding(true);
|
||||
this.$router.push({ name: 'onboarding' });
|
||||
},
|
||||
setHasVisitedOnboarding () {
|
||||
if (localStorage.hasOwnProperty('hasVisitedOnboarding')) {
|
||||
return JSON.parse(window.localStorage.getItem('hasVisitedOnboarding'));
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
updateHasVisitedOnboarding (visited) {
|
||||
this.hasVisitedOnboarding = visited;
|
||||
localStorage.hasVisitedOnboarding = JSON.stringify(visited);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
@import 'new-dashboard/styles/variables';
|
||||
|
||||
button.button--ghost {
|
||||
position: relative;
|
||||
|
||||
&.not-visited::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
right: -12px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: $onboarding-notification;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<canvas id="congrats-confetti"></canvas>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import 'confetti-js';
|
||||
|
||||
export default {
|
||||
name: 'Confetti',
|
||||
mounted () {
|
||||
const canvasBoundingRect = this.$el.getBoundingClientRect();
|
||||
|
||||
const confettiSettings = {
|
||||
target: 'congrats-confetti',
|
||||
max: 250,
|
||||
size: 0.5,
|
||||
animate: true,
|
||||
props: ['square'],
|
||||
colors: [
|
||||
[41, 106, 166],
|
||||
[140, 193, 64],
|
||||
[219, 44, 111],
|
||||
[88, 178, 85],
|
||||
[56, 78, 144],
|
||||
[84, 189, 199],
|
||||
[59, 82, 147],
|
||||
[134, 191, 65],
|
||||
[89, 177, 70],
|
||||
[127, 72, 139],
|
||||
[241, 170, 73],
|
||||
[142, 194, 64],
|
||||
[63, 92, 154],
|
||||
[241, 170, 73],
|
||||
[90, 72, 139],
|
||||
[210, 34, 126],
|
||||
[209, 36, 42],
|
||||
[66, 187, 206],
|
||||
[211, 32, 122],
|
||||
[201, 215, 56],
|
||||
[212, 22, 124],
|
||||
[125, 42, 121],
|
||||
[252, 236, 38],
|
||||
[152, 122, 169]
|
||||
],
|
||||
clock: 35,
|
||||
rotate: true,
|
||||
width: canvasBoundingRect.width,
|
||||
height: canvasBoundingRect.height
|
||||
};
|
||||
|
||||
// confetti-js exports function to global scope 🤦♂️
|
||||
this.confetti = new window.ConfettiGenerator(confettiSettings);
|
||||
this.confetti.render();
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.confetti.clear();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import 'new-dashboard/styles/variables';
|
||||
|
||||
#congrats-confetti {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: $onboarding-congrats__height;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<div class="footer">
|
||||
<div class="container u-flex u-flex__justify--end">
|
||||
<button class="button button--ghost js-prev" v-if="showPrevButton" @click="prev">
|
||||
{{ $t('Wizards.footer.back') }}
|
||||
</button>
|
||||
<button class="button is-primary js-next" v-if="showNextButton" @click="next">
|
||||
{{ $t('Wizards.footer.next', { nextStep: this.stepNames[this.currentStep] }) }}
|
||||
</button>
|
||||
<button class="button is-primary js-goToDashboard" v-if="!showNextButton" @click="goToDashboard">
|
||||
{{ $t('Wizards.footer.goToDashboard') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Footer',
|
||||
props: {
|
||||
stepNames: Array,
|
||||
currentStep: {
|
||||
type: Number,
|
||||
default: 1
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
maxSteps () {
|
||||
return this.stepNames.length;
|
||||
},
|
||||
showNextButton () {
|
||||
return this.currentStep < this.maxSteps;
|
||||
},
|
||||
showPrevButton () {
|
||||
return this.currentStep > 1;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
prev () {
|
||||
this.$emit('goToStep', this.currentStep - 1);
|
||||
},
|
||||
next () {
|
||||
this.$emit('goToStep', this.currentStep + 1);
|
||||
},
|
||||
goToDashboard () {
|
||||
this.$router.push({ name: 'home' });
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import 'new-dashboard/styles/variables';
|
||||
|
||||
.footer {
|
||||
padding: 2em;
|
||||
background: $white;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,174 @@
|
||||
<template>
|
||||
<div class="header">
|
||||
<div class="container">
|
||||
<ul class="breadcrumbs" :class="[stepNames.length > 1 ? 'u-flex__justify--between' : 'u-flex__justify--center']">
|
||||
<li class="breadcrumbs__item"
|
||||
:class="{ 'current' : isCurrentStep(index) }"
|
||||
v-for="(stepName, index) in stepNames" :key="stepName">
|
||||
<a :class="stepClass(index)" href="javascript:void 0" @click="goToStep(index)">
|
||||
<span class="breadcrumbs__checkpoint" :class="{ 'current' : isCurrentStep(index) }">
|
||||
<span class="breadcrumbs__text u-hideMobile">{{ stepName }}</span>
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Header',
|
||||
props: {
|
||||
stepNames: Array,
|
||||
currentStep: Number
|
||||
},
|
||||
methods: {
|
||||
isCurrentStep (index) {
|
||||
return index + 1 === this.currentStep;
|
||||
},
|
||||
goToStep (stepNumber) {
|
||||
this.$emit('goToStep', stepNumber + 1);
|
||||
},
|
||||
stepClass (stepNumber) {
|
||||
return `step-${stepNumber + 1}`;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import 'new-dashboard/styles/variables';
|
||||
|
||||
$timeline__border-width: 1px;
|
||||
$timeline__border-color: #DDD;
|
||||
$timeline__border-transition: 0.25s;
|
||||
$bullet__transition: 0.12s;
|
||||
$bullet__border-color: #D3E6FA;
|
||||
$transition__timing-function: cubic-bezier(0.4, 0.01, 0.165, 0.99);
|
||||
|
||||
.header {
|
||||
display: block;
|
||||
padding-top: 2.375em;
|
||||
background: $white;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.breadcrumbs {
|
||||
display: flex;
|
||||
max-width: 90%;
|
||||
margin: 0 auto 1.5em;
|
||||
|
||||
&__item {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-top: $timeline__border-width solid $primary-color;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
width: 0;
|
||||
border-top: $timeline__border-width solid transparent;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: -$timeline__border-width;
|
||||
right: 0;
|
||||
width: 0%;
|
||||
transition: width $timeline__border-transition $transition__timing-function;
|
||||
border-top: $timeline__border-width solid $timeline__border-color;
|
||||
}
|
||||
|
||||
&.current {
|
||||
&::after {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: -$timeline__border-width;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
transition: width $timeline__border-transition $transition__timing-function;
|
||||
border-top: $timeline__border-width solid $timeline__border-color;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
& ~ .breadcrumbs__item {
|
||||
border-color: $timeline__border-color;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: -$timeline__border-width;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
transition: none;
|
||||
border-top: $timeline__border-width solid $timeline__border-color;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
& ~ .breadcrumbs__item .breadcrumbs__checkpoint {
|
||||
top: -4px;
|
||||
left: -4px;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
background: $timeline__border-color;
|
||||
}
|
||||
|
||||
& ~ .breadcrumbs__item .breadcrumbs__text {
|
||||
color: $text__color--secondary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__text {
|
||||
position: absolute;
|
||||
top: 24px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
color: $text__color;
|
||||
font-size: 0.75em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__checkpoint {
|
||||
display: block;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: -4px;
|
||||
left: -4px;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
transition: all $bullet__transition $transition__timing-function;
|
||||
border-radius: 50%;
|
||||
background: $primary-color;
|
||||
|
||||
&.current {
|
||||
transition: all $bullet__transition $transition__timing-function;
|
||||
transition-delay: $timeline__border-transition;
|
||||
box-shadow: 0 0 0 6px $bullet__border-color;
|
||||
|
||||
.breadcrumbs__text {
|
||||
transition: all $bullet__transition $transition__timing-function;
|
||||
color: $text__color;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<div :class="{'container u-width--100 u-mt--64 u-mb--80': !fullWidth}" class="u-flex__grow--1">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'WizardStep',
|
||||
props: {
|
||||
fullWidth: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<h2 class="title is-body u-mb--24 u-flex u-flex__align--center">
|
||||
<span class="icon icon--large">
|
||||
<slot name="icon"></slot>
|
||||
</span>
|
||||
|
||||
{{title}}
|
||||
</h2>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'StepTitle',
|
||||
props: {
|
||||
title: String
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import 'new-dashboard/styles/variables';
|
||||
|
||||
.icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin-right: 16px;
|
||||
margin-left: -40px;
|
||||
}
|
||||
|
||||
.icon--large {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
</style>
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<div class="u-flex card-container">
|
||||
<slot></slot>
|
||||
|
||||
<div class="u-flex u-ml--24 u-flex__direction--column u-flex__justify--between u-flex__grow--1">
|
||||
<div class="u-flex u-flex__direction--column">
|
||||
<h3 class="title is-caption u-mb--8">{{content.title}}</h3>
|
||||
<p class="text is-caption u-mb--8">{{content.subtitle}}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="content.link && content.button" class="u-flex u-flex__justify--end">
|
||||
<a :href="content.link" target="_blank" class="button button--ghost button--outline">
|
||||
{{content.button}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'TutorialCard',
|
||||
props: {
|
||||
content: Object
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import 'new-dashboard/styles/variables';
|
||||
|
||||
.card-container {
|
||||
height: 180px;
|
||||
padding: 24px;
|
||||
border: 1px solid #DDD;
|
||||
}
|
||||
</style>
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<section class="onboarding-welcome">
|
||||
<Modal name="onboarding-welcome" :isOpen="true" @closeModal="closeModal">
|
||||
<div class="distributor gtm-onboarding">
|
||||
<div class="header">
|
||||
<div class="container">
|
||||
{{ $t('Wizards.Distributor.headerTitle') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container u-pt--36">
|
||||
<h1 class="u-pr--10 u-pl--10 u-mb--40 title is-body is-semibold">
|
||||
{{ $t('Wizards.Distributor.title') }}
|
||||
</h1>
|
||||
|
||||
<div class="grid u-flex__justify--between">
|
||||
<div class="grid-cell grid-cell--col4 grid-cell--col6--tablet grid-cell--col12--mobile">
|
||||
<Selector
|
||||
:title="$t('Wizards.cartovl.title')"
|
||||
:text="$t('Wizards.cartovl.subtitle')"
|
||||
:tags="$t('Wizards.cartovl.tags')"
|
||||
@click.native="openOnboarding('carto-vl')"
|
||||
iconModifier="map"></Selector>
|
||||
<Warning v-if="!hasEngine"></Warning>
|
||||
</div>
|
||||
|
||||
<div class="grid-cell grid-cell--col4 grid-cell--col6--tablet grid-cell--col12--mobile">
|
||||
<Selector
|
||||
:title="$t('Wizards.cartoframes.title')"
|
||||
:text="$t('Wizards.cartoframes.subtitle')"
|
||||
:tags="$t('Wizards.cartoframes.tags')"
|
||||
@click.native="openOnboarding('cartoframes')"
|
||||
iconModifier="notebook"></Selector>
|
||||
<Warning v-if="!hasEngine"></Warning>
|
||||
</div>
|
||||
|
||||
<div class="grid-cell grid-cell--col4 grid-cell--col6--tablet grid-cell--col12--mobile">
|
||||
<Selector
|
||||
:title="$t('Wizards.builder.title')"
|
||||
:text="$t('Wizards.builder.subtitle')"
|
||||
:tags="$t('Wizards.builder.tags')"
|
||||
@click.native="openOnboarding('builder')"
|
||||
iconModifier="window"></Selector>
|
||||
</div>
|
||||
</div>
|
||||
<!--
|
||||
<p class="grid-cell u-mt--48 is-small">{{ $t('Wizards.Distributor.feedback.text') }}
|
||||
<a href="https://docs.google.com/forms/d/e/1FAIpQLScBQUWd-TP3Qy514DOCNg-KoLrViHijUR5giLAMS-3jmDnrPg/viewform" target="_blank">
|
||||
{{ $t('Wizards.Distributor.feedback.link') }}</a>
|
||||
</p>
|
||||
-->
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<router-view name="onboarding" />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Selector from 'new-dashboard/components/Onboarding/distributor/Selector';
|
||||
import Warning from 'new-dashboard/components/Onboarding/distributor/Warning';
|
||||
import Modal from 'new-dashboard/components/Modal.vue';
|
||||
|
||||
export default {
|
||||
name: 'OnboardingWelcome',
|
||||
components: {
|
||||
Selector,
|
||||
Warning,
|
||||
Modal
|
||||
},
|
||||
computed: {
|
||||
hasEngine () {
|
||||
return this.$store.getters['user/hasEngine'];
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openOnboarding (onboardingId) {
|
||||
this.$router.push({
|
||||
name: 'onboarding-open',
|
||||
params: { onboardingId },
|
||||
hash: '#step-1'
|
||||
});
|
||||
},
|
||||
closeModal () {
|
||||
this.$router.push({ name: 'home' });
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
@import 'new-dashboard/styles/variables';
|
||||
|
||||
.distributor {
|
||||
height: 100%;
|
||||
background-color: $onboarding__bg-color;
|
||||
color: $text__color;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 98px;
|
||||
background: $white;
|
||||
}
|
||||
|
||||
.button {
|
||||
&.button--ghost {
|
||||
margin-right: 36px;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<div class="selector" tabindex="0">
|
||||
<div>
|
||||
<h3 class="selector__title title is-body is-medium u-mb--8" :class="iconClass">
|
||||
{{ this.title }}
|
||||
</h3>
|
||||
|
||||
<p class="text is-caption">{{ this.text }}</p>
|
||||
</div>
|
||||
|
||||
<ul class="selector__tags">
|
||||
<li class="selector__tags-item title" v-for="tag in tags" :key="tag">{{ tag }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Selector',
|
||||
props: {
|
||||
title: String,
|
||||
text: String,
|
||||
tags: {
|
||||
type: Array,
|
||||
default () {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
iconModifier: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
iconClass () {
|
||||
return (this.iconModifier ? `selector__title-icon selector__title-icon--${this.iconModifier}` : '');
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import 'new-dashboard/styles/variables';
|
||||
|
||||
.selector {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
max-width: 460px;
|
||||
height: 340px;
|
||||
padding: 1.5em;
|
||||
border: 1px solid $border-color--dark;
|
||||
border-radius: 2px;
|
||||
background-color: $white;
|
||||
cursor: pointer;
|
||||
|
||||
&__title-icon {
|
||||
&::before {
|
||||
content: '';
|
||||
display: block;
|
||||
position: relative;
|
||||
top: -16px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin-top: 16px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
&--map {
|
||||
&::before {
|
||||
content: '';
|
||||
height: 50px;
|
||||
background-image: url('../../../assets/icons/onboarding/locationmap.svg');
|
||||
}
|
||||
}
|
||||
|
||||
&--notebook {
|
||||
&::before {
|
||||
content: '';
|
||||
background-image: url('../../../assets/icons/onboarding/notebook.svg');
|
||||
}
|
||||
}
|
||||
|
||||
&--window {
|
||||
&::before {
|
||||
content: '';
|
||||
background-image: url('../../../assets/icons/onboarding/window.svg');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__tags {
|
||||
&-item {
|
||||
display: inline-block;
|
||||
margin-top: 8px;
|
||||
margin-right: 8px;
|
||||
padding: 6px 16px;
|
||||
border: 1px solid $onboarding-tags__border-color;
|
||||
border-radius: 18px;
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
outline: none;
|
||||
box-shadow: $card__shadow;
|
||||
|
||||
.selector__title {
|
||||
color: $primary-color;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div class="warning">
|
||||
<span class="text is-small">
|
||||
{{$t(`Wizards.Distributor.engineWarning.text`)}} <a target="_blank" class="warning__link" :href="$t(`Wizards.Distributor.engineWarning.link`)">{{$t(`Wizards.Distributor.engineWarning.linkText`)}}</a>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Warning'
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import 'new-dashboard/styles/variables';
|
||||
|
||||
.warning {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
max-width: 460px;
|
||||
height: 34px;
|
||||
padding: 1.3em;
|
||||
border-bottom-right-radius: 2px;
|
||||
border-bottom-left-radius: 2px;
|
||||
background-color: $primary-color;
|
||||
color: $white;
|
||||
|
||||
&__link {
|
||||
color: $white;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<div class="datasetcard u-width--100">
|
||||
<div class="u-flex u-flex__align--center">
|
||||
<div class="datasetcard-icon"></div>
|
||||
<div class="title-container">
|
||||
<h3 class="text is-caption is-txtGrey u-ellipsis" title="Populated places simple">{{content.title}}</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="u-flex u-flex__align--center">
|
||||
<span class="text is-small is-txtSoftGrey u-mr--24" :title="content.rows">{{content.rows}}</span>
|
||||
<span class="text is-small is-txtSoftGrey u-mr--48">{{content.size}}</span>
|
||||
<a download :href="content.link" class="button button--ghost button--outline">{{content.button}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'DatasetCard_21',
|
||||
props: {
|
||||
content: Object
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
@import 'new-dashboard/styles/variables';
|
||||
|
||||
.datasetcard {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
padding: 0 16px;
|
||||
background-color: $white;
|
||||
|
||||
.datasetcard-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
margin-right: 16px;
|
||||
border-radius: 2px;
|
||||
background-color: #F2F6F9;
|
||||
background-image: url(../../../../assets/icons/datasets/data-types/dots.svg);
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: 18px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<div class="u-width--100 u-pr--10 u-pl--10">
|
||||
<StepTitle :title="$t(`Wizards.builder.step1.title`)">
|
||||
<template slot="icon">
|
||||
<img svg-inline src="../../../../assets/icons/onboarding/summary.svg"/>
|
||||
</template>
|
||||
</StepTitle>
|
||||
<img class="u-width--100" src="../../../../assets/images/onboarding/builder_step1@2x.png"/>
|
||||
<p v-html="$t(`Wizards.builder.step1.text1`)" class="text is-body u-mb--16 u-mt--24"></p>
|
||||
<p v-html="$t(`Wizards.builder.step1.text2`)" class="text is-body u-mb--64"></p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import StepTitle from 'new-dashboard/components/Onboarding/components/StepTitle.vue';
|
||||
|
||||
export default {
|
||||
name: 'Step1',
|
||||
stepName: 'Intro',
|
||||
components: {
|
||||
StepTitle
|
||||
}
|
||||
};
|
||||
</script>
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<div class="u-width--100 u-pr--10 u-pl--10">
|
||||
<StepTitle :title="$t(`Wizards.builder.sections.summary`)">
|
||||
<template slot="icon">
|
||||
<img svg-inline src="../../../../assets/icons/onboarding/summary.svg"/>
|
||||
</template>
|
||||
</StepTitle>
|
||||
<p v-html="$t(`Wizards.builder.step2.summary.text`)" class="text is-body u-mb--64"></p>
|
||||
|
||||
<StepTitle :title="$t(`Wizards.builder.sections.howto`)">
|
||||
<template slot="icon">
|
||||
<img svg-inline src="../../../../assets/icons/onboarding/howto.svg"/>
|
||||
</template>
|
||||
</StepTitle>
|
||||
<ol class="u-list__style--decimal u-list__position--inside">
|
||||
<li>
|
||||
<p v-html="$t(`Wizards.builder.step2.howto.instruction1`)" class="text is-caption u-mb--16 u-inlineflex"></p>
|
||||
<DatasetCard :content="$t(`Wizards.builder.step2.cards.dataset1`)"></DatasetCard>
|
||||
</li>
|
||||
<li>
|
||||
<p v-html="$t(`Wizards.builder.step2.howto.instruction2`)" class="text is-caption u-mt--24 u-mb--24 u-inlineflex"></p>
|
||||
</li>
|
||||
<li>
|
||||
<p v-html="$t(`Wizards.builder.step2.howto.instruction3`)" class="text is-caption u-mb--16 u-inlineflex"></p>
|
||||
<img class="u-width--100" src="../../../../assets/images/onboarding/builder_step2@2x.png" alt="Screenshot connect dataset modal in builder">
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<StepTitle :title="$t(`Wizards.builder.sections.extras`)" class="u-mt--64">
|
||||
<template slot="icon">
|
||||
<img svg-inline src="../../../../assets/icons/onboarding/extras.svg"/>
|
||||
</template>
|
||||
</StepTitle>
|
||||
<p v-html="$t(`Wizards.builder.step2.extras.resource1`)" class="text is-caption u-mb--16 u-mb--64"></p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import StepTitle from 'new-dashboard/components/Onboarding/components/StepTitle.vue';
|
||||
import DatasetCard from './DatasetCard_21.vue';
|
||||
|
||||
export default {
|
||||
name: 'Step2',
|
||||
stepName: 'Import your data',
|
||||
components: {
|
||||
StepTitle,
|
||||
DatasetCard
|
||||
}
|
||||
};
|
||||
</script>
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<div class="u-width--100 u-pr--10 u-pl--10">
|
||||
<StepTitle :title="$t(`Wizards.builder.sections.summary`)">
|
||||
<template slot="icon">
|
||||
<img svg-inline src="../../../../assets/icons/onboarding/summary.svg"/>
|
||||
</template>
|
||||
</StepTitle>
|
||||
<p v-html="$t(`Wizards.builder.step3.summary.text`)" class="text is-body u-mb--64"></p>
|
||||
|
||||
<StepTitle :title="$t(`Wizards.builder.sections.howto`)">
|
||||
<template slot="icon">
|
||||
<img svg-inline src="../../../../assets/icons/onboarding/howto.svg"/>
|
||||
</template>
|
||||
</StepTitle>
|
||||
<ol class="u-list__style--decimal u-list__position--inside">
|
||||
<li>
|
||||
<p v-html="$t(`Wizards.builder.step3.howto.instruction1`)" class="text is-caption u-mt--24 u-inline"></p>
|
||||
<img class="u-width--100 u-mt--16" src="../../../../assets/images/onboarding/builder_step3@2x.png" alt="Screenshot dataset preview in builder">
|
||||
</li>
|
||||
<li>
|
||||
<p v-html="$t(`Wizards.builder.step3.howto.instruction2`)" class="text is-caption u-mb--24 u-mt--24 u-inlineflex"></p>
|
||||
</li>
|
||||
<li>
|
||||
<p v-html="$t(`Wizards.builder.step3.howto.instruction3`)" class="text is-caption u-mb--24 u-inlineflex"></p>
|
||||
</li>
|
||||
<li>
|
||||
<p v-html="$t(`Wizards.builder.step3.howto.instruction4`)" class="text is-caption u-inlineflex"></p>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<StepTitle :title="'Extras'" class="u-mt--64">
|
||||
<template slot="icon">
|
||||
<img svg-inline src="../../../../assets/icons/onboarding/extras.svg"/>
|
||||
</template>
|
||||
</StepTitle>
|
||||
<p v-html="$t(`Wizards.builder.step3.extras.resource1`)" class="text is-caption u-mb--16 u-mb--64"></p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import StepTitle from 'new-dashboard/components/Onboarding/components/StepTitle.vue';
|
||||
|
||||
export default {
|
||||
name: 'Step3',
|
||||
stepName: 'Explore the dataset',
|
||||
components: {
|
||||
StepTitle
|
||||
}
|
||||
};
|
||||
</script>
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<div class="u-width--100 u-pr--10 u-pl--10">
|
||||
<StepTitle :title="$t(`Wizards.builder.sections.summary`)">
|
||||
<template slot="icon">
|
||||
<img svg-inline src="../../../../assets/icons/onboarding/summary.svg"/>
|
||||
</template>
|
||||
</StepTitle>
|
||||
<p v-html="$t(`Wizards.builder.step4.summary.text`)" class="text is-body u-mb--64"></p>
|
||||
|
||||
<StepTitle :title="$t(`Wizards.builder.sections.howto`)">
|
||||
<template slot="icon">
|
||||
<img svg-inline src="../../../../assets/icons/onboarding/howto.svg"/>
|
||||
</template>
|
||||
</StepTitle>
|
||||
<ol class="u-list__style--decimal u-list__position--inside">
|
||||
<li class="u-mt--24">
|
||||
<p v-html="$t(`Wizards.builder.step4.howto.instruction1`)" class="text is-caption u-inline"></p>
|
||||
</li>
|
||||
<li class="u-mt--24">
|
||||
<p v-html="$t(`Wizards.builder.step4.howto.instruction2`)" class="text is-caption u-inline"></p>
|
||||
<img class="u-width--100 u-mt--16" src="../../../../assets/images/onboarding/builder_step4@2x.png" alt="Screenshot map in builder">
|
||||
</li>
|
||||
<li class="u-mt--24">
|
||||
<p v-html="$t(`Wizards.builder.step4.howto.instruction3`)" class="text is-caption u-inline"></p>
|
||||
</li>
|
||||
<li class="u-mt--24">
|
||||
<p v-html="$t(`Wizards.builder.step4.howto.instruction4`)" class="text is-caption u-inline"></p>
|
||||
<img class="u-width--100 u-mt--16" src="../../../../assets/images/onboarding/builder_step4b@2x.png" alt="Screenshot styled map in builder">
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<StepTitle :title="$t(`Wizards.builder.sections.extras`)" class="u-mt--64">
|
||||
<template slot="icon">
|
||||
<img svg-inline src="../../../../assets/icons/onboarding/extras.svg"/>
|
||||
</template>
|
||||
</StepTitle>
|
||||
<p v-html="$t(`Wizards.builder.step4.extras.resource1`)" class="text is-caption"></p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import StepTitle from 'new-dashboard/components/Onboarding/components/StepTitle.vue';
|
||||
|
||||
export default {
|
||||
name: 'Step4',
|
||||
stepName: 'Create a map',
|
||||
components: {
|
||||
StepTitle
|
||||
}
|
||||
};
|
||||
</script>
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<div class="u-width--100 u-pr--10 u-pl--10">
|
||||
<StepTitle :title="$t(`Wizards.builder.sections.summary`)">
|
||||
<template slot="icon">
|
||||
<img svg-inline src="../../../../assets/icons/onboarding/summary.svg"/>
|
||||
</template>
|
||||
</StepTitle>
|
||||
<p v-html="$t(`Wizards.builder.step5.summary.text`)" class="text is-body u-mb--64"></p>
|
||||
|
||||
<StepTitle :title="$t(`Wizards.builder.sections.howto`)">
|
||||
<template slot="icon">
|
||||
<img svg-inline src="../../../../assets/icons/onboarding/howto.svg"/>
|
||||
</template>
|
||||
</StepTitle>
|
||||
<ol class="u-list__style--decimal u-list__position--inside">
|
||||
<li class="u-mb--24">
|
||||
<p v-html="$t(`Wizards.builder.step5.howto.instruction1`)" class="text is-caption u-inline"></p>
|
||||
</li>
|
||||
<li class="u-mb--24">
|
||||
<p v-html="$t(`Wizards.builder.step5.howto.instruction2`)" class="text is-caption u-inline"></p>
|
||||
</li>
|
||||
<li class="u-mb--24">
|
||||
<p v-html="$t(`Wizards.builder.step5.howto.instruction3`)" class="text is-caption u-inline"></p>
|
||||
</li>
|
||||
<li class="u-mb--24">
|
||||
<p v-html="$t(`Wizards.builder.step5.howto.instruction4`)" class="text is-caption u-inline"></p>
|
||||
<img class="u-width--100 u-mt--16" src="../../../../assets/images/onboarding/builder_step5@2x.png" alt="Screenshot publish map page in builder">
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<StepTitle :title="$t(`Wizards.builder.sections.extras`)" class="u-mt--64">
|
||||
<template slot="icon">
|
||||
<img svg-inline src="../../../../assets/icons/onboarding/extras.svg"/>
|
||||
</template>
|
||||
</StepTitle>
|
||||
<p v-html="$t(`Wizards.builder.step5.extras.resource1`)" class="text is-caption"></p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import StepTitle from 'new-dashboard/components/Onboarding/components/StepTitle.vue';
|
||||
|
||||
export default {
|
||||
name: 'Step5',
|
||||
stepName: 'Publish your map',
|
||||
components: {
|
||||
StepTitle
|
||||
}
|
||||
};
|
||||
</script>
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<div class="congrats">
|
||||
<div class="congrats__hero">
|
||||
<Confetti></Confetti>
|
||||
<div class="congrats__hero-content">
|
||||
<img svg-inline src="../../../../assets/icons/onboarding/congrats.svg" alt="">
|
||||
<h2 class="title is-body u-mb--16 u-mt--24 ">{{ $t(`Wizards.builder.step6.congrats.title`) }}</h2>
|
||||
<p v-html="$t(`Wizards.builder.step6.congrats.message1`)" class="text is-caption"></p>
|
||||
<p v-html="$t(`Wizards.builder.step6.congrats.message2`, { url: downloadUrl })" class="text is-caption"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container u-mt--64 u-pb--80">
|
||||
<StepTitle :title="$t(`Wizards.builder.sections.learnmore`)">
|
||||
<template slot="icon">
|
||||
<img svg-inline src="../../../../assets/icons/onboarding/learn.svg"/>
|
||||
</template>
|
||||
</StepTitle>
|
||||
<p v-html="$t(`Wizards.builder.step6.extras.resource1`)" class="text is-caption u-mt--16 u-mb--24"></p>
|
||||
<TutorialCard :content="$t(`Wizards.builder.step6.cards.card1`)" class="u-mb--16">
|
||||
<img src="../../../../assets/images/onboarding/builder_card1.png" width="240" height="130" alt="Style features by category sample image"/>
|
||||
</TutorialCard>
|
||||
<TutorialCard :content="$t(`Wizards.builder.step6.cards.card2`)" class="u-mb--16">
|
||||
<img src="../../../../assets/images/onboarding/builder_card2.png" width="240" height="130" alt="Add hover popups sample image"/>
|
||||
</TutorialCard>
|
||||
<TutorialCard :content="$t(`Wizards.builder.step6.cards.card3`)">
|
||||
<img src="../../../../assets/images/onboarding/builder_card3.png" width="240" height="130" alt="Animate the features sample image"/>
|
||||
</TutorialCard>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import StepTitle from 'new-dashboard/components/Onboarding/components/StepTitle.vue';
|
||||
import TutorialCard from 'new-dashboard/components/Onboarding/components/TutorialCard.vue';
|
||||
import Confetti from 'new-dashboard/components/Onboarding/components/Confetti.vue';
|
||||
import { getAssetsBaseUrl } from 'new-dashboard/utils/assets-version';
|
||||
|
||||
export default {
|
||||
name: 'Step6',
|
||||
stepName: 'Download',
|
||||
components: {
|
||||
StepTitle,
|
||||
TutorialCard,
|
||||
Confetti
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
downloadUrl: getAssetsBaseUrl() + 'unversioned/onboarding/Myfirstmap.carto'
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
@import 'new-dashboard/styles/variables';
|
||||
|
||||
.congrats {
|
||||
background-color: $white;
|
||||
|
||||
&__hero {
|
||||
display: flex;
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: $onboarding-congrats__height;
|
||||
background-color: $onboarding-congrats__bg-color;
|
||||
}
|
||||
|
||||
&__hero-content {
|
||||
display: flex;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,8 @@
|
||||
import { importFilesFrom } from 'new-dashboard/utils/dynamic-import';
|
||||
|
||||
const folderContext = require.context('./', true, /Step.*\.vue$/);
|
||||
|
||||
const wizardName = 'wizardBuilder';
|
||||
const steps = importFilesFrom(folderContext, /^\.\/(.*)\.vue$/);
|
||||
|
||||
export default { steps, wizardName };
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<div class="u-width--100 u-pr--10 u-pl--10">
|
||||
<StepTitle :title="$t(`Wizards.cartovl.step1.title`)">
|
||||
<template slot="icon">
|
||||
<img svg-inline src="../../../../assets/icons/onboarding/summary.svg"/>
|
||||
</template>
|
||||
</StepTitle>
|
||||
|
||||
<InjectableIframe :content="map" width="940" height="416"></InjectableIframe>
|
||||
|
||||
<p v-html="$t(`Wizards.cartovl.step1.text1`)" class="text is-body u-mb--16 u-mt--24"></p>
|
||||
<p v-html="$t(`Wizards.cartovl.step1.text2`)" class="text is-body u-mb--16 u-mt--24"></p>
|
||||
<p v-html="$t(`Wizards.cartovl.step1.text3`)" class="text is-body u-mb--64"></p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import StepTitle from 'new-dashboard/components/Onboarding/components/StepTitle.vue';
|
||||
import InjectableIframe from 'new-dashboard/components/InjectableIframe';
|
||||
import map from 'new-dashboard/assets/resources/onboarding/cartovl.html';
|
||||
|
||||
export default {
|
||||
name: 'Step1',
|
||||
stepName: 'Intro',
|
||||
components: {
|
||||
InjectableIframe,
|
||||
StepTitle
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
map
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<div class="u-width--100 u-pr--10 u-pl--10">
|
||||
<StepTitle :title="$t(`Wizards.cartovl.sections.summary`)">
|
||||
<template slot="icon">
|
||||
<img svg-inline src="../../../../assets/icons/onboarding/summary.svg"/>
|
||||
</template>
|
||||
</StepTitle>
|
||||
<p v-html="$t(`Wizards.cartovl.step2.summary`)" class="text is-body u-mb--64"></p>
|
||||
|
||||
<StepTitle :title="$t(`Wizards.cartovl.sections.howto`)">
|
||||
<template slot="icon">
|
||||
<img svg-inline src="../../../../assets/icons/onboarding/howto.svg"/>
|
||||
</template>
|
||||
</StepTitle>
|
||||
<ol class="u-list__style--decimal u-list__position--inside">
|
||||
<li>
|
||||
<p v-html="$t(`Wizards.cartovl.step2.howto.instruction1`)" class="text is-caption u-inlineflex"></p>
|
||||
<CodeBlock :code="codeBlock1" language="htmlmixed"></CodeBlock>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<StepTitle :title="$t(`Wizards.cartovl.sections.extras`)" class="u-mt--64">
|
||||
<template slot="icon">
|
||||
<img svg-inline src="../../../../assets/icons/onboarding/extras.svg"/>
|
||||
</template>
|
||||
</StepTitle>
|
||||
<p v-html="$t(`Wizards.cartovl.step2.extras.resource1`)" class="text is-caption u-mb--24"></p>
|
||||
<p v-html="$t(`Wizards.cartovl.step2.extras.resource2`)" class="text is-caption u-mb--64"></p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CodeBlock from 'new-dashboard/components/Code/CodeBlock.vue';
|
||||
import StepTitle from 'new-dashboard/components/Onboarding/components/StepTitle.vue';
|
||||
|
||||
export default {
|
||||
name: 'Step2',
|
||||
stepName: 'Load CARTO VL',
|
||||
components: {
|
||||
CodeBlock,
|
||||
StepTitle
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
codeBlock1
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const codeBlock1 =
|
||||
`<html>
|
||||
<head>
|
||||
<title>CARTO VL Example</title>
|
||||
<!-- Load CARTO VL JS -->
|
||||
<script src="https://libs.cartocdn.com/carto-vl/v1.2.4/carto-vl.min.js"><\/script>
|
||||
<!-- Load Mapbox GL -->
|
||||
<script src="https://api.tiles.mapbox.com/mapbox-gl-js/v0.52.0/mapbox-gl.js"><\/script>
|
||||
<link href="https://api.tiles.mapbox.com/mapbox-gl-js/v0.52.0/mapbox-gl.css" rel="stylesheet">
|
||||
|
||||
<!-- Your style will go here -->
|
||||
</head>
|
||||
<body>
|
||||
<!-- Your map will go here -->
|
||||
</body>
|
||||
</html>`;
|
||||
</script>
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<div class="u-width--100 u-pr--10 u-pl--10">
|
||||
<StepTitle :title="$t(`Wizards.cartovl.sections.summary`)">
|
||||
<template slot="icon">
|
||||
<img svg-inline src="../../../../assets/icons/onboarding/summary.svg"/>
|
||||
</template>
|
||||
</StepTitle>
|
||||
<p v-html="$t(`Wizards.cartovl.step3.summary`)" class="text is-body u-mb--64"></p>
|
||||
|
||||
<StepTitle :title="$t(`Wizards.cartovl.sections.howto`)">
|
||||
<template slot="icon">
|
||||
<img svg-inline src="../../../../assets/icons/onboarding/howto.svg"/>
|
||||
</template>
|
||||
</StepTitle>
|
||||
<ol class="u-list__style--decimal u-list__position--inside">
|
||||
<li>
|
||||
<p v-html="$t(`Wizards.cartovl.step3.howto.instruction1`)" class="text is-caption u-inline"></p>
|
||||
<CodeBlock :code="codeBlock1" language="htmlmixed" class="u-mt--16"></CodeBlock>
|
||||
</li>
|
||||
<li class="u-mt--36">
|
||||
<p v-html="$t(`Wizards.cartovl.step3.howto.instruction2`)" class="text is-caption u-inline"></p>
|
||||
<CodeBlock :code="codeBlock2" language="htmlmixed"></CodeBlock>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CodeBlock from 'new-dashboard/components/Code/CodeBlock.vue';
|
||||
import StepTitle from 'new-dashboard/components/Onboarding/components/StepTitle.vue';
|
||||
|
||||
export default {
|
||||
name: 'Step3',
|
||||
stepName: 'Define container',
|
||||
components: {
|
||||
CodeBlock,
|
||||
StepTitle
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
codeBlock1,
|
||||
codeBlock2
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const codeBlock1 = `<div id="map"><\/div>`;
|
||||
|
||||
const codeBlock2 =
|
||||
`<style>
|
||||
#map {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%
|
||||
}
|
||||
<\/style>`;
|
||||
</script>
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<div class="u-width--100 u-pr--10 u-pl--10">
|
||||
<StepTitle :title="$t(`Wizards.cartovl.sections.summary`)">
|
||||
<template slot="icon">
|
||||
<img svg-inline src="../../../../assets/icons/onboarding/summary.svg"/>
|
||||
</template>
|
||||
</StepTitle>
|
||||
<p v-html="$t(`Wizards.cartovl.step4.summary`)" class="text is-body u-mb--64"></p>
|
||||
|
||||
<StepTitle :title="$t(`Wizards.cartovl.sections.howto`)">
|
||||
<template slot="icon">
|
||||
<img svg-inline src="../../../../assets/icons/onboarding/howto.svg"/>
|
||||
</template>
|
||||
</StepTitle>
|
||||
<ol class="u-list__style--decimal u-list__position--inside">
|
||||
<li>
|
||||
<p v-html="$t(`Wizards.cartovl.step4.howto.instruction1`)" class="text is-caption u-inline"></p>
|
||||
<CodeBlock :code="codeBlock1" language="htmlmixed"></CodeBlock>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<StepTitle :title="$t(`Wizards.cartovl.sections.extras`)" class="u-mt--64">
|
||||
<template slot="icon">
|
||||
<img svg-inline src="../../../../assets/icons/onboarding/extras.svg"/>
|
||||
</template>
|
||||
</StepTitle>
|
||||
<p v-html="$t(`Wizards.cartovl.step4.extras.resource1`)" class="text is-caption u-mb--16"></p>
|
||||
<div class="u-flex u-flex__justify--between u-mb--16 u-mb--64">
|
||||
<img src="../../../../assets/images/onboarding/voyager@2x.png" width="306" height="140" alt="Voyager basemap sample"/>
|
||||
<img src="../../../../assets/images/onboarding/positron@2x.png" width="306" height="140" alt="Positron basemap sample"/>
|
||||
<img src="../../../../assets/images/onboarding/darkmatter@2x.png" width="306" height="140" alt="Dark Matter basemap sample"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CodeBlock from 'new-dashboard/components/Code/CodeBlock.vue';
|
||||
import StepTitle from 'new-dashboard/components/Onboarding/components/StepTitle.vue';
|
||||
|
||||
export default {
|
||||
name: 'Step4',
|
||||
stepName: 'Create map',
|
||||
components: {
|
||||
CodeBlock,
|
||||
StepTitle
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
codeBlock1
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const codeBlock1 =
|
||||
`<script>
|
||||
const map = new mapboxgl.Map({
|
||||
container: 'map',
|
||||
style: carto.basemaps.voyager,
|
||||
center: [0, 30],
|
||||
zoom: 2
|
||||
})
|
||||
<\/script>`;
|
||||
</script>
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<div class="u-width--100 u-pr--10 u-pl--10">
|
||||
<StepTitle :title="$t(`Wizards.cartovl.sections.summary`)">
|
||||
<template slot="icon">
|
||||
<img svg-inline src="../../../../assets/icons/onboarding/summary.svg"/>
|
||||
</template>
|
||||
</StepTitle>
|
||||
<p v-html="$t(`Wizards.cartovl.step5.summary`)" class="text is-body u-mb--64"></p>
|
||||
|
||||
<StepTitle :title="$t(`Wizards.cartovl.sections.howto`)">
|
||||
<template slot="icon">
|
||||
<img svg-inline src="../../../../assets/icons/onboarding/howto.svg"/>
|
||||
</template>
|
||||
</StepTitle>
|
||||
<ol class="u-list__style--decimal u-list__position--inside">
|
||||
<li>
|
||||
<p v-html="$t(`Wizards.cartovl.step5.howto.instruction1`)" class="text is-caption u-inline"></p>
|
||||
<CodeBlock :code="codeBlock1"></CodeBlock>
|
||||
</li>
|
||||
<li>
|
||||
<p v-html="$t(`Wizards.cartovl.step5.howto.instruction2`)" class="text is-caption u-inline"></p>
|
||||
<CodeBlock :code="codeBlock2"></CodeBlock>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<StepTitle :title="$t(`Wizards.cartovl.sections.extras`)" class="u-mt--64">
|
||||
<template slot="icon">
|
||||
<img svg-inline src="../../../../assets/icons/onboarding/extras.svg"/>
|
||||
</template>
|
||||
</StepTitle>
|
||||
<p v-html="$t(`Wizards.cartovl.step5.extras.resource1`)" class="text is-caption u-mt--16 u-mb--64"></p>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import CodeBlock from 'new-dashboard/components/Code/CodeBlock.vue';
|
||||
import StepTitle from 'new-dashboard/components/Onboarding/components/StepTitle.vue';
|
||||
|
||||
export default {
|
||||
name: 'Step5',
|
||||
stepName: 'Add data layer',
|
||||
components: {
|
||||
CodeBlock,
|
||||
StepTitle
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
codeBlock1,
|
||||
codeBlock2
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Code Blocks to be rendered in steps
|
||||
const codeBlock1 =
|
||||
`carto.setDefaultAuth({
|
||||
username: 'public',
|
||||
apiKey: 'default_public'
|
||||
})`;
|
||||
|
||||
const codeBlock2 =
|
||||
`const viz = new carto.Viz();
|
||||
const source = new carto.source.Dataset('world_ports')
|
||||
const layer = new carto.Layer('layer', source, viz);
|
||||
layer.addTo(map);`;
|
||||
</script>
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<div class="u-width--100 u-pr--10 u-pl--10">
|
||||
<StepTitle :title="$t(`Wizards.cartovl.sections.summary`)">
|
||||
<template slot="icon">
|
||||
<img svg-inline src="../../../../assets/icons/onboarding/summary.svg"/>
|
||||
</template>
|
||||
</StepTitle>
|
||||
<p v-html="$t(`Wizards.cartovl.step6.summary`)" class="text is-body u-mb--64"></p>
|
||||
|
||||
<StepTitle :title="$t(`Wizards.cartovl.sections.howto`)">
|
||||
<template slot="icon">
|
||||
<img svg-inline src="../../../../assets/icons/onboarding/howto.svg"/>
|
||||
</template>
|
||||
</StepTitle>
|
||||
<ol class="u-list__style--decimal u-list__position--inside">
|
||||
<li>
|
||||
<p v-html="$t(`Wizards.cartovl.step6.howto.instruction1`)" class="text is-caption u-inline"></p>
|
||||
<CodeBlock :code="codeBlock1"></CodeBlock>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<StepTitle :title="$t(`Wizards.cartovl.sections.extras`)" class="u-mt--64">
|
||||
<template slot="icon">
|
||||
<img svg-inline src="../../../../assets/icons/onboarding/extras.svg"/>
|
||||
</template>
|
||||
</StepTitle>
|
||||
<p v-html="$t(`Wizards.cartovl.step6.extras.resource1`)" class="text is-caption u-mt--16 u-mb--64"></p>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import CodeBlock from 'new-dashboard/components/Code/CodeBlock.vue';
|
||||
import StepTitle from 'new-dashboard/components/Onboarding/components/StepTitle.vue';
|
||||
|
||||
export default {
|
||||
name: 'Step6',
|
||||
stepName: 'Style features',
|
||||
components: {
|
||||
CodeBlock,
|
||||
StepTitle
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
codeBlock1
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Code Blocks to be rendered in steps
|
||||
const codeBlock1 =
|
||||
`const viz= new carto.Viz(\`
|
||||
width: 4
|
||||
strokeWidth: 0.5
|
||||
strokeColor: black
|
||||
color: rgb(10,185,107)
|
||||
\`);`;
|
||||
</script>
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<div class="congrats">
|
||||
<div class="congrats__hero">
|
||||
<Confetti></Confetti>
|
||||
<div class="congrats__hero-content">
|
||||
<img svg-inline src="../../../../assets/icons/onboarding/congrats.svg" alt="">
|
||||
<h2 class="title is-body u-mb--16 u-mt--24 ">{{ $t(`Wizards.cartovl.step7.congrats.title`) }}</h2>
|
||||
<p v-html="$t(`Wizards.cartovl.step7.congrats.message1`)" class="text is-caption"></p>
|
||||
<p v-html="$t(`Wizards.cartovl.step7.congrats.message2`, { url: downloadUrl })" class="text is-caption"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container u-mt--64 u-pb--80">
|
||||
<StepTitle :title="$t(`Wizards.cartovl.sections.learnmore`)">
|
||||
<template slot="icon">
|
||||
<img svg-inline src="../../../../assets/icons/onboarding/learn.svg"/>
|
||||
</template>
|
||||
</StepTitle>
|
||||
<p v-html="$t(`Wizards.cartovl.step7.extras.resource1`)" class="text is-caption u-mt--16 u-mb--64"></p>
|
||||
<TutorialCard :content="$t(`Wizards.cartovl.step7.cards.card1`)" class="u-mb--16">
|
||||
<img src="../../../../assets/images/onboarding/stylefeatures.png" width="240" height="130" alt="Style features by category sample image"/>
|
||||
</TutorialCard>
|
||||
<TutorialCard :content="$t(`Wizards.cartovl.step7.cards.card2`)" class="u-mb--16">
|
||||
<img src="../../../../assets/images/onboarding/hoverpopups.png" width="240" height="130" alt="Add hover popups sample image"/>
|
||||
</TutorialCard>
|
||||
<TutorialCard :content="$t(`Wizards.cartovl.step7.cards.card3`)">
|
||||
<img src="../../../../assets/images/onboarding/animatefeatures.png" width="240" height="130" alt="Animate the features sample image"/>
|
||||
</TutorialCard>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import StepTitle from 'new-dashboard/components/Onboarding/components/StepTitle.vue';
|
||||
import TutorialCard from 'new-dashboard/components/Onboarding/components/TutorialCard.vue';
|
||||
import Confetti from 'new-dashboard/components/Onboarding/components/Confetti.vue';
|
||||
import { getAssetsBaseUrl } from 'new-dashboard/utils/assets-version';
|
||||
|
||||
export default {
|
||||
name: 'Step7',
|
||||
stepName: 'Download',
|
||||
components: {
|
||||
StepTitle,
|
||||
TutorialCard,
|
||||
Confetti
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
downloadUrl: getAssetsBaseUrl() + 'unversioned/onboarding/cartovl.zip'
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
@import 'new-dashboard/styles/variables';
|
||||
|
||||
.congrats {
|
||||
background-color: $white;
|
||||
|
||||
&__hero {
|
||||
display: flex;
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: $onboarding-congrats__height;
|
||||
background-color: $onboarding-congrats__bg-color;
|
||||
}
|
||||
|
||||
&__hero-content {
|
||||
display: flex;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,8 @@
|
||||
import { importFilesFrom } from 'new-dashboard/utils/dynamic-import';
|
||||
|
||||
const folderContext = require.context('./', true, /Step.*\.vue$/);
|
||||
|
||||
const wizardName = 'wizardCARTOVL';
|
||||
const steps = importFilesFrom(folderContext, /^\.\/(.*)\.vue$/);
|
||||
|
||||
export default { steps, wizardName };
|
||||
+203
@@ -0,0 +1,203 @@
|
||||
<template>
|
||||
<div class="u-width--100 u-pt--32 u-pr--10 u-pb--80 u-pl--10">
|
||||
<div class="circle"></div>
|
||||
<div class="placeholder">
|
||||
<div class="placeholder__img"></div>
|
||||
<div class="placeholder__shadow"></div>
|
||||
</div>
|
||||
|
||||
<ul class="tag__list">
|
||||
<li class="tag__item title">Python</li>
|
||||
<li class="tag__item title">CARTOframes</li>
|
||||
</ul>
|
||||
<h1 v-html="$t(`Wizards.cartoframes.step1.title`)" class="heading title is-subheader"></h1>
|
||||
|
||||
<div class="block">
|
||||
<StepTitle :title="$t(`Wizards.cartoframes.step1.sections.summary`)">
|
||||
<template slot="icon">
|
||||
<img svg-inline src="../../../../assets/icons/onboarding/summary.svg"/>
|
||||
</template>
|
||||
</StepTitle>
|
||||
<ul class="list">
|
||||
<li class="list__item text">
|
||||
<span>{{ $t(`Wizards.cartoframes.step1.text1`) }}</span>
|
||||
</li>
|
||||
<li class="list__item text">
|
||||
<span>{{ $t(`Wizards.cartoframes.step1.text2`) }}</span>
|
||||
</li>
|
||||
<li class="list__item text">
|
||||
<span>{{ $t(`Wizards.cartoframes.step1.text3`) }}</span><span class="u-ml--4 is-italic">{{ $t(`Wizards.cartoframes.step1.text4`) }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="block">
|
||||
<StepTitle :title="$t(`Wizards.cartoframes.step1.sections.howto`)">
|
||||
<template slot="icon">
|
||||
<img svg-inline src="../../../../assets/icons/onboarding/howto.svg"/>
|
||||
</template>
|
||||
</StepTitle>
|
||||
<CodeBlock :code="codeBlock1" language="python" :lineNumbers="false" theme="default"></CodeBlock>
|
||||
<InjectableIframe :content="map" width="100%" height="436"></InjectableIframe>
|
||||
</div>
|
||||
|
||||
<div class="footer hangar">
|
||||
<button class="underlined-link title is-caption is-txtCartoframes" @click="goToDashboard">
|
||||
<span>{{ $t('Wizards.cartoframes.step1.footer.returnToDashboard') }}</span>
|
||||
</button>
|
||||
<a href="https://carto.com/developers/cartoframes/" class="button button--arrow is-cartoframes" target="_blank" @click="goToDashboard">
|
||||
<span>{{ $t('Wizards.cartoframes.step1.footer.learnMore') }}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import StepTitle from 'new-dashboard/components/Onboarding/components/StepTitle.vue';
|
||||
import CodeBlock from 'new-dashboard/components/Code/CodeBlock.vue';
|
||||
import InjectableIframe from 'new-dashboard/components/InjectableIframe';
|
||||
import map from 'new-dashboard/assets/resources/onboarding/cartoframes.html';
|
||||
|
||||
export default {
|
||||
name: 'Step1',
|
||||
stepName: 'Add spatial data to your data science workflow',
|
||||
components: {
|
||||
StepTitle,
|
||||
CodeBlock,
|
||||
InjectableIframe
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
codeBlock1,
|
||||
map
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
goToDashboard () {
|
||||
this.$router.push({ name: 'home' });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const codeBlock1 =
|
||||
`from cartoframes.auth import set_default_credentials
|
||||
from cartoframes.viz import Map
|
||||
from cartoframes.viz.helpers import color_bins_layer
|
||||
|
||||
set_default_credentials('johnsmith', '1a2b3c4d5e6f7g8h9i0j')
|
||||
|
||||
masters_map = Map(color_bins_layer(
|
||||
'higher_edu_by_county',
|
||||
'pct_higher_ed',
|
||||
'% Population with Masters degree')
|
||||
)
|
||||
|
||||
masters_map.publish('higher_edu_by_county_map')`;
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import 'new-dashboard/styles/variables';
|
||||
@import 'new-dashboard/styles/hangar/_variables';
|
||||
|
||||
.circle {
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 120vw;
|
||||
height: 120vw;
|
||||
transform: translate(44vw, -71.5vw);
|
||||
border-radius: 50%;
|
||||
background-color: $cartoframes;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translate(-70px, -10px);
|
||||
|
||||
&__img {
|
||||
width: 460px;
|
||||
height: 330px;
|
||||
border-radius: 4px;
|
||||
background-image: url('../../../../assets/images/onboarding/cartoframes@2x.png');
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
&__shadow {
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
top: 24px;
|
||||
left: 24px;
|
||||
width: 412px;
|
||||
height: 326px;
|
||||
opacity: 0.24;
|
||||
background: $neutral--800;
|
||||
filter: blur(24px);
|
||||
}
|
||||
}
|
||||
|
||||
.tag {
|
||||
&__list {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
&__item {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
margin-right: 16px;
|
||||
padding: 7px 16px;
|
||||
border-radius: 18px;
|
||||
background-color: rgba($cartoframes, 0.08);
|
||||
color: $cartoframes;
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 18px;
|
||||
background-color: #FFF;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.heading {
|
||||
max-width: 300px;
|
||||
margin-bottom: 150px;
|
||||
}
|
||||
|
||||
.block {
|
||||
margin-bottom: 96px;
|
||||
}
|
||||
|
||||
.list {
|
||||
padding-left: 16px;
|
||||
|
||||
&__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 16px;
|
||||
line-height: 38px;
|
||||
|
||||
&::before {
|
||||
content: '\00b7';
|
||||
margin-right: 8px;
|
||||
font-size: 60px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
</style>
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import { importFilesFrom } from 'new-dashboard/utils/dynamic-import';
|
||||
|
||||
const folderContext = require.context('./', true, /Step.*\.vue$/);
|
||||
|
||||
const wizardName = 'wizardCARTOframes';
|
||||
const showFooter = false;
|
||||
const steps = importFilesFrom(folderContext, /^\.\/(.*)\.vue$/);
|
||||
|
||||
export default { steps, showFooter, wizardName };
|
||||
@@ -0,0 +1,75 @@
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
step: 1,
|
||||
isModalOpen: true
|
||||
};
|
||||
},
|
||||
mounted () {
|
||||
this.setStepNumber();
|
||||
},
|
||||
watch: {
|
||||
$route () {
|
||||
this.setStepNumber();
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
totalStepCount () {
|
||||
return this.stepNames.length;
|
||||
},
|
||||
stepComponent () {
|
||||
return `Step${this.step}`;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
goToStep (stepNum) {
|
||||
this.$router.push({
|
||||
hash: `#step-${stepNum}`
|
||||
});
|
||||
},
|
||||
isCurrentStep (stepNum) {
|
||||
return this.step === stepNum;
|
||||
},
|
||||
openModal () {
|
||||
this.isModalOpen = true;
|
||||
},
|
||||
closeModal () {
|
||||
this.isModalOpen = false;
|
||||
this.step = 1;
|
||||
this.$emit('closeModal');
|
||||
},
|
||||
setStepNumber () {
|
||||
const {stepNumber, isValid} = parseHashParameterWithNumber(this.$route.hash);
|
||||
|
||||
if (stepNumber > this.totalStepCount || !isValid) {
|
||||
return this.$router.push({ hash: '#step-1' });
|
||||
}
|
||||
|
||||
this.step = stepNumber;
|
||||
},
|
||||
getStepGTMString (stepNumber) {
|
||||
return `gtm-${this.wizardName}-step${stepNumber}`;
|
||||
}
|
||||
},
|
||||
updated () {
|
||||
if (this.$refs.wizard) {
|
||||
this.$refs.wizard.scrollTo({ top: 0, left: 0 });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function parseHashParameterWithNumber (hash, token = 'step-') {
|
||||
const stepIndex = hash.indexOf(token);
|
||||
const includesStep = stepIndex > -1;
|
||||
|
||||
if (!includesStep) {
|
||||
return;
|
||||
}
|
||||
|
||||
const stepNumber = +hash.substring(stepIndex + token.length);
|
||||
|
||||
return {
|
||||
isValid: Number.isInteger(stepNumber),
|
||||
stepNumber: stepNumber
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<component :is="componentName" @closeModal="closeModal"></component>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { createWizard } from '../wizard/createWizard';
|
||||
|
||||
// Wizards
|
||||
import CARTOVLWizard from '../tutorials/CARTOVL';
|
||||
import CARTOframesWizard from '../tutorials/CARTOframes';
|
||||
import BuilderWizard from '../tutorials/Builder';
|
||||
|
||||
export default {
|
||||
name: 'Wizard',
|
||||
components: {
|
||||
builder: createWizard(BuilderWizard.wizardName, BuilderWizard.steps),
|
||||
cartoframes: createWizard(CARTOframesWizard.wizardName, CARTOframesWizard.steps, CARTOframesWizard.showFooter),
|
||||
'carto-vl': createWizard(CARTOVLWizard.wizardName, CARTOVLWizard.steps)
|
||||
},
|
||||
computed: {
|
||||
componentName () {
|
||||
return this.$route.params.onboardingId;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
closeModal () {
|
||||
this.$router.push({ name: 'home' });
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,83 @@
|
||||
import Step from 'new-dashboard/components/Onboarding/components/Step.vue';
|
||||
import Header from 'new-dashboard/components/Onboarding/components/Header.vue';
|
||||
import Footer from 'new-dashboard/components/Onboarding/components/Footer.vue';
|
||||
import Modal from 'new-dashboard/components/Modal.vue';
|
||||
|
||||
import mixin from '../tutorials/mixin';
|
||||
import template from './template.html';
|
||||
import './style.scss';
|
||||
|
||||
const defaultComponentConfiguration = {
|
||||
template,
|
||||
components: {
|
||||
Step,
|
||||
Header,
|
||||
Footer,
|
||||
Modal
|
||||
},
|
||||
mixins: [mixin]
|
||||
};
|
||||
|
||||
/**
|
||||
* Onboarding Wizard Creation
|
||||
*
|
||||
* Function to create a new wizard from a defined steps
|
||||
*
|
||||
* @param {string} name - Name for the generated wizard.
|
||||
* @param {array} steps - Wizard's steps definition.
|
||||
*
|
||||
* Each step within the array needs to follow this format:
|
||||
* { stepName: <String>, component: <ComponentObject> }
|
||||
*
|
||||
* stepName: Name that the step takes in the wizard
|
||||
* component: Vue component object definition for the Step
|
||||
*/
|
||||
|
||||
export function createWizard (name = '', steps = {}, showFooter = true) {
|
||||
const stepsLength = Object.keys(steps).length;
|
||||
|
||||
if (!steps || !stepsLength) {
|
||||
throw new Error('Steps components are needed to generate a Wizard');
|
||||
}
|
||||
|
||||
const wizardConfiguration = generateWizardConfiguration(name, steps, showFooter);
|
||||
|
||||
return {
|
||||
...defaultComponentConfiguration,
|
||||
...wizardConfiguration
|
||||
};
|
||||
}
|
||||
|
||||
function generateWizardConfiguration (name, steps, showFooter) {
|
||||
// Generate Wizard configuration
|
||||
const { components, stepNames } = parseSteps(steps);
|
||||
|
||||
const wizardComponents = {
|
||||
...defaultComponentConfiguration.components,
|
||||
...components
|
||||
};
|
||||
|
||||
return {
|
||||
name,
|
||||
components: wizardComponents,
|
||||
data () {
|
||||
return {
|
||||
wizardName: name,
|
||||
stepNames,
|
||||
showFooter
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function parseSteps (steps) {
|
||||
const components = {};
|
||||
const stepNames = [];
|
||||
|
||||
Object.values(steps).forEach(component => {
|
||||
components[component.name] = component;
|
||||
stepNames.push(component.stepName);
|
||||
});
|
||||
|
||||
return { components, stepNames };
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
@import 'new-dashboard/styles/variables';
|
||||
|
||||
.wizard {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
height: 100%;
|
||||
overflow: hidden auto;
|
||||
background-color: $onboarding__bg-color;
|
||||
|
||||
// Fix for Safari Scrolling with GPU acceleration
|
||||
/* stylelint-disable */
|
||||
transform: translate3d(0, 0, 0);
|
||||
/* stylelint-enable */
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<Modal :name="wizardName" :isOpen="isModalOpen" @closeModal="closeModal">
|
||||
<div ref="wizard" class="wizard">
|
||||
<Header :stepNames="stepNames" :currentStep="step" @goToStep="goToStep"></Header>
|
||||
|
||||
<template v-for="step in totalStepCount">
|
||||
<Step
|
||||
v-if="isCurrentStep(step)"
|
||||
:class="getStepGTMString(step)"
|
||||
:stepNum="step"
|
||||
:key="step"
|
||||
:fullWidth="step === stepNames.length && stepNames.length > 1">
|
||||
<component :is="stepComponent"></component>
|
||||
</Step>
|
||||
</template>
|
||||
|
||||
<Footer v-if="showFooter"
|
||||
:stepNames="stepNames"
|
||||
:currentStep="step"
|
||||
@goToStep="goToStep"
|
||||
@closeModal="closeModal"></Footer>
|
||||
</div>
|
||||
</Modal>
|
||||
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<section class="page">
|
||||
<slot />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'Page'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import 'new-dashboard/styles/variables';
|
||||
|
||||
.page {
|
||||
position: relative;
|
||||
min-height: calc(100vh - 392px);
|
||||
padding: 64px 0 120px;
|
||||
border-bottom: 1px solid $border-color;
|
||||
|
||||
&--welcome,
|
||||
&--settings,
|
||||
&--data {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.page.page__sticky-subheader {
|
||||
padding: $subheader__height + 64px 0 120px;
|
||||
}
|
||||
</style>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user