Initial commit

This commit is contained in:
zhongjin
2020-06-15 10:58:47 +08:00
commit 4f1dfe7564
8590 changed files with 1516878 additions and 0 deletions
@@ -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>&nbsp;
<span class="text is-small">{{ app.username }}</span>
</div>
<div class="u-mb--16">
<span class="appform__label">{{ $t(`OAuthAppsPage.form.clientId`) }}</span>&nbsp;
<span class="text is-small">{{ app.client_id }}</span>
</div>
<div class="u-mb--16">
<span class="appform__label">{{ $t(`OAuthAppsPage.form.clientSecret`) }}</span>&nbsp;
<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">&nbsp;{{ $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">&nbsp;{{ $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>
@@ -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>
@@ -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)">,&nbsp;</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)">,&nbsp;</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">,&#32;</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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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 };
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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 };
@@ -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>
@@ -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