1
0
Fork 0
mirror of https://gitlab.com/bramw/baserow.git synced 2025-03-23 16:23:25 +00:00

Resolve "Web frontend refactor"

This commit is contained in:
Bram Wiepjes 2020-03-27 13:19:43 +00:00
parent 63aa85eaa3
commit be2c5c4d4f
149 changed files with 1019 additions and 874 deletions
backend
src/baserow/contrib/database/fields
tests/baserow/contrib/database/api/v0/rows
web-frontend

View file

@ -1,3 +1,5 @@
from decimal import Decimal
from django.db import models
from django.core.exceptions import ValidationError
@ -35,6 +37,8 @@ class NumberFieldType(FieldType):
serializer_field_names = ['number_type', 'number_decimal_places', 'number_negative']
def prepare_value_for_db(self, instance, value):
if instance.number_type == NUMBER_TYPE_DECIMAL:
value = Decimal(value)
if not instance.number_negative and value < 0:
raise ValidationError(f'The value for field {instance.id} cannot be '
f'negative.')

View file

@ -1,3 +1,5 @@
from decimal import Decimal
import pytest
from django.shortcuts import reverse
@ -241,6 +243,31 @@ def test_update_row(api_client, data_fixture):
assert getattr(row_2, f'field_{number_field.id}') == 50
assert not getattr(row_2, f'field_{boolean_field.id}')
table_3 = data_fixture.create_database_table(user=user)
decimal_field = data_fixture.create_number_field(
table=table_3, order=0, name='Price', number_type='DECIMAL',
number_decimal_places=2
)
model_3 = table_3.get_model()
row_3 = model_3.objects.create()
url = reverse('api_v0:database:rows:item', kwargs={
'table_id': table_3.id,
'row_id': row_3.id
})
response = api_client.patch(
url,
{f'field_{decimal_field.id}': 10.22},
format='json',
HTTP_AUTHORIZATION=f'JWT {token}'
)
response_json = response.json()
assert response.status_code == 200
assert response_json[f'field_{decimal_field.id}'] == '10.22'
row_3.refresh_from_db()
assert getattr(row_3, f'field_{decimal_field.id}') == Decimal('10.22')
@pytest.mark.django_db
def test_delete_row(api_client, data_fixture):

View file

@ -1,25 +0,0 @@
# baserow-web-frontend
> Baserow web frontend
## Build Setup
``` bash
# install dependencies
$ yarn install
# serve with hot reload at localhost:3000
$ yarn run dev
# build for production and launch server
$ yarn run build
$ yarn start
# generate static project
$ yarn run generate
# lint
```
For detailed explanation on how things work, checkout [Nuxt.js docs](https://nuxtjs.org).

View file

@ -1,81 +0,0 @@
<template>
<Modal>
<h2 class="box-title">Create new {{ applicationType.name | lowercase }}</h2>
<div v-if="error" class="alert alert-error alert-has-icon">
<div class="alert-icon">
<i class="fas fa-exclamation"></i>
</div>
<div class="alert-title">{{ errorTitle }}</div>
<p class="alert-content">{{ errorMessage }}</p>
</div>
<component
:is="applicationType.getApplicationFormComponent()"
ref="applicationForm"
@submitted="submitted"
>
<div class="actions">
<div class="align-right">
<button
class="button button-large"
:class="{ 'button-loading': loading }"
:disabled="loading"
>
Add {{ applicationType.name | lowercase }}
</button>
</div>
</div>
</component>
</Modal>
</template>
<script>
import modal from '@/mixins/modal'
export default {
name: 'CreateApplicationModal',
mixins: [modal],
props: {
applicationType: {
type: Object,
required: true
}
},
data() {
return {
loading: false,
error: false,
errorTitle: '',
errorMessage: ''
}
},
methods: {
submitted(values) {
this.loading = true
this.error = false
this.$store
.dispatch('application/create', {
type: this.applicationType.type,
values: values
})
.then(() => {
this.loading = false
this.$emit('created')
this.hide()
})
.catch(error => {
this.loading = false
if (error.handler) {
const message = error.handler.getMessage('group')
this.error = true
this.errorTitle = message.title
this.errorMessage = message.message
error.handler.handled()
} else {
throw error
}
})
}
}
}
</script>

View file

@ -1,77 +1,11 @@
export default {
mode: 'universal',
head: {
title: 'Baserow',
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' }
],
link: [
{
rel: 'icon',
type: 'image/png',
href: '/img/favicon_16.png',
sizes: '16x16'
},
{
rel: 'icon',
type: 'image/png',
href: '/img/favicon_32.png',
sizes: '32x32'
},
{
rel: 'icon',
type: 'image/png',
href: '/img/favicon_48.png',
sizes: '64x64'
},
{
rel: 'icon',
type: 'image/png',
href: '/img/favicon_192.png',
sizes: '192x192'
}
]
},
/**
* Customize the progress-bar color
*/
loading: { color: '#fff' },
/**
* Global CSS
*/
css: ['@/assets/scss/default.scss'],
/**
* Plugins to load before mounting the App
*/
plugins: [
{ src: '@/plugins/global.js' },
{ src: '@/plugins/client.js' },
{ src: '@/plugins/auth.js' },
{ src: '@/plugins/vuelidate.js' }
],
/**
* Nuxt.js modules
*/
modules: [
'@nuxtjs/axios',
'cookie-universal-nuxt',
'@/modules/database/module.js'
],
router: {
middleware: ['authentication']
},
modules: ['@/modules/core/module.js', '@/modules/database/module.js'],
env: {
// The API base url, this will be prepended to the urls of the remote calls.
baseUrl: 'http://backend:8000/api/v0',
// If the API base url must different at the browser side it can be changed
// If the API base url must different at the client side it can be changed
// here.
publicBaseUrl: 'http://localhost:8000/api/v0'
}

View file

@ -1,4 +1,7 @@
/** This file can be used in combination with intellij idea so the @ path resolves **/
/**
* This file can be used in combination with intellij idea so the @baserow path
* resolves.
*/
const path = require('path')
@ -7,10 +10,7 @@ module.exports = {
extensions: ['.js', '.json', '.vue', '.ts'],
root: path.resolve(__dirname),
alias: {
'@': path.resolve(__dirname),
'@@': path.resolve(__dirname),
'~': path.resolve(__dirname),
'~~': path.resolve(__dirname)
'@baserow': path.resolve(__dirname)
}
}
}

View file

@ -1,4 +1,4 @@
import ApplicationForm from '@/components/sidebar/ApplicationForm'
import ApplicationForm from '@baserow/modules/core/components/application/ApplicationForm'
/**
* The application type base class that can be extended when creating a plugin

View file

@ -5,8 +5,8 @@
</template>
<script>
import { isElement } from '@/utils/dom'
import MoveToBody from '@/mixins/moveToBody'
import { isElement } from '@baserow/modules/core/utils/dom'
import MoveToBody from '@baserow/modules/core/mixins/moveToBody'
export default {
name: 'Context',

View file

@ -34,7 +34,7 @@
</template>
<script>
import { isElement } from '@/utils/dom'
import { isElement } from '@baserow/modules/core/utils/dom'
// @TODO focus on tab
export default {

View file

@ -10,7 +10,7 @@
</template>
<script>
import { focusEnd } from '@/utils/dom'
import { focusEnd } from '@baserow/modules/core/utils/dom'
export default {
name: 'Editable',

View file

@ -0,0 +1,23 @@
<template>
<div v-if="error.visible" class="alert alert-error alert-has-icon">
<div class="alert-icon">
<i class="fas fa-exclamation"></i>
</div>
<div class="alert-title">{{ error.title }}</div>
<p class="alert-content">{{ error.message }}</p>
</div>
</template>
<script>
/**
* This component works the best if the parent has the error mixin.
*/
export default {
props: {
error: {
required: true,
type: Object
}
}
}
</script>

View file

@ -15,7 +15,7 @@
</template>
<script>
import MoveToBody from '@/mixins/moveToBody'
import MoveToBody from '@baserow/modules/core/mixins/moveToBody'
export default {
name: 'Modal',

View file

@ -20,7 +20,7 @@
</template>
<script>
import { floor, ceil } from '@/utils/number'
import { floor, ceil } from '@baserow/modules/core/utils/number'
/**
* This component will render custom scrollbars to a scrollable div. They will

View file

@ -26,7 +26,7 @@
<script>
import { required } from 'vuelidate/lib/validators'
import form from '@/mixins/form'
import form from '@baserow/modules/core/mixins/form'
export default {
name: 'ApplicationForm',

View file

@ -25,8 +25,8 @@
<script>
import { mapState } from 'vuex'
import CreateApplicationModal from '@/components/sidebar/CreateApplicationModal'
import context from '@/mixins/context'
import CreateApplicationModal from '@baserow/modules/core/components/application/CreateApplicationModal'
import context from '@baserow/modules/core/mixins/context'
export default {
name: 'CreateApplicationContext',

View file

@ -0,0 +1,63 @@
<template>
<Modal>
<h2 class="box-title">Create new {{ applicationType.name | lowercase }}</h2>
<Error :error="error"></Error>
<component
:is="applicationType.getApplicationFormComponent()"
ref="applicationForm"
@submitted="submitted"
>
<div class="actions">
<div class="align-right">
<button
class="button button-large"
:class="{ 'button-loading': loading }"
:disabled="loading"
>
Add {{ applicationType.name | lowercase }}
</button>
</div>
</div>
</component>
</Modal>
</template>
<script>
import modal from '@baserow/modules/core/mixins/modal'
import error from '@baserow/modules/core/mixins/error'
export default {
name: 'CreateApplicationModal',
mixins: [modal, error],
props: {
applicationType: {
type: Object,
required: true
}
},
data() {
return {
loading: false
}
},
methods: {
async submitted(values) {
this.loading = true
this.hideError()
try {
await this.$store.dispatch('application/create', {
type: this.applicationType.type,
values: values
})
this.loading = false
this.$emit('created')
this.hide()
} catch (error) {
this.loading = false
this.handleError(error, 'application')
}
}
}
}
</script>

View file

@ -1,6 +1,7 @@
<template>
<Modal>
<h2 class="box-title">Create new group</h2>
<Error :error="error"></Error>
<GroupForm ref="groupForm" @submitted="submitted">
<div class="actions">
<div class="align-right">
@ -18,31 +19,33 @@
</template>
<script>
import GroupForm from './GroupForm'
import modal from '@baserow/modules/core/mixins/modal'
import error from '@baserow/modules/core/mixins/error'
import modal from '@/mixins/modal'
import GroupForm from './GroupForm'
export default {
name: 'CreateGroupModal',
components: { GroupForm },
mixins: [modal],
mixins: [modal, error],
data() {
return {
loading: false
}
},
methods: {
submitted(values) {
async submitted(values) {
this.loading = true
this.$store
.dispatch('group/create', values)
.then(() => {
this.loading = false
this.hide()
})
.catch(() => {
this.loading = false
})
this.hideError()
try {
await this.$store.dispatch('group/create', values)
this.loading = false
this.hide()
} catch (error) {
this.loading = false
this.handleError(error, 'group')
}
}
}
}

View file

@ -26,7 +26,7 @@
<script>
import { required } from 'vuelidate/lib/validators'
import form from '@/mixins/form'
import form from '@baserow/modules/core/mixins/form'
export default {
name: 'GroupForm',

View file

@ -39,9 +39,9 @@
<script>
import { mapGetters, mapState } from 'vuex'
import CreateGroupModal from '@/components/group/CreateGroupModal'
import GroupsContextItem from '@/components/group/GroupsContextItem'
import context from '@/mixins/context'
import CreateGroupModal from '@baserow/modules/core/components/group/CreateGroupModal'
import GroupsContextItem from '@baserow/modules/core/components/group/GroupsContextItem'
import context from '@baserow/modules/core/mixins/context'
export default {
name: 'GroupsContext',

View file

@ -41,7 +41,7 @@
</template>
<script>
import { notifyIf } from '@/utils/error'
import { notifyIf } from '@baserow/modules/core/utils/error'
export default {
name: 'GroupsContextItem',
@ -59,51 +59,46 @@ export default {
this.$refs.context.hide()
this.$refs.rename.edit()
},
renameGroup(group, event) {
async renameGroup(group, event) {
this.setLoading(group, true)
this.$store
.dispatch('group/update', {
try {
await this.$store.dispatch('group/update', {
group,
values: {
name: event.value
}
})
.catch(error => {
this.$refs.rename.set(event.oldValue)
notifyIf(error, 'group')
})
.then(() => {
this.setLoading(group, false)
})
} catch (error) {
this.$refs.rename.set(event.oldValue)
notifyIf(error, 'group')
}
this.setLoading(group, false)
},
selectGroup(group) {
async selectGroup(group) {
this.setLoading(group, true)
this.$store
.dispatch('group/select', group)
.then(() => {
this.$emit('selected')
})
.catch(error => {
notifyIf(error, 'group')
})
.then(() => {
this.setLoading(group, false)
})
try {
await this.$store.dispatch('group/select', group)
this.$emit('selected')
} catch (error) {
notifyIf(error, 'group')
}
this.setLoading(group, false)
},
deleteGroup(group) {
async deleteGroup(group) {
this.$refs.context.hide()
this.setLoading(group, true)
this.$store
.dispatch('group/delete', group)
.catch(error => {
notifyIf(error, 'group')
})
.then(() => {
this.setLoading(group, false)
})
try {
await this.$store.dispatch('group/delete', group)
} catch (error) {
notifyIf(error, 'group')
}
this.setLoading(group, false)
}
}
}

View file

@ -11,7 +11,7 @@
<script>
import { mapState } from 'vuex'
import Notification from '@/components/notifications/Notification'
import Notification from '@baserow/modules/core/components/notifications/Notification'
export default {
name: 'Notifications',

View file

@ -31,8 +31,8 @@
<script>
import { mapGetters, mapState } from 'vuex'
import SidebarApplication from '@/components/sidebar/SidebarApplication'
import CreateApplicationContext from '@/components/sidebar/CreateApplicationContext'
import SidebarApplication from '@baserow/modules/core/components/sidebar/SidebarApplication'
import CreateApplicationContext from '@baserow/modules/core/components/application/CreateApplicationContext'
export default {
name: 'Sidebar',

View file

@ -57,7 +57,7 @@
</template>
<script>
import { notifyIf } from '@/utils/error'
import { notifyIf } from '@baserow/modules/core/utils/error'
export default {
name: 'SidebarApplication',
@ -78,31 +78,32 @@ export default {
this.$refs.context.hide()
this.$refs.rename.edit()
},
renameApplication(application, event) {
async renameApplication(application, event) {
this.setLoading(application, true)
this.$store
.dispatch('application/update', {
try {
await this.$store.dispatch('application/update', {
application,
values: {
name: event.value
}
})
.catch(error => {
this.$refs.rename.set(event.oldValue)
notifyIf(error, 'application')
})
.then(() => {
this.setLoading(application, false)
})
} catch (error) {
this.$refs.rename.set(event.oldValue)
notifyIf(error, 'application')
}
this.setLoading(application, false)
},
selectApplication(application) {
async selectApplication(application) {
// If there is no route associated with the application we just change the
// selected state.
if (application._.type.routeName === null) {
this.$store.dispatch('application/select', application).catch(error => {
try {
await this.$store.dispatch('application/select', application)
} catch (error) {
notifyIf(error, 'group')
})
}
return
}
@ -125,18 +126,17 @@ export default {
}
)
},
deleteApplication(application) {
async deleteApplication(application) {
this.$refs.context.hide()
this.setLoading(application, true)
this.$store
.dispatch('application/delete', application)
.catch(error => {
notifyIf(error, 'application')
})
.then(() => {
this.setLoading(application, false)
})
try {
await this.$store.dispatch('application/delete', application)
} catch (error) {
notifyIf(error, 'application')
}
this.setLoading(application, false)
},
getSelectedApplicationComponent(application) {
const type = this.$store.getters['application/getType'](application.type)

View file

View file

@ -0,0 +1,34 @@
export default {
title: 'Baserow',
titleTemplate: '%s // Baserow',
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' }
],
link: [
{
rel: 'icon',
type: 'image/png',
href: '/img/favicon_16.png',
sizes: '16x16'
},
{
rel: 'icon',
type: 'image/png',
href: '/img/favicon_32.png',
sizes: '32x32'
},
{
rel: 'icon',
type: 'image/png',
href: '/img/favicon_48.png',
sizes: '64x64'
},
{
rel: 'icon',
type: 'image/png',
href: '/img/favicon_192.png',
sizes: '192x192'
}
]
}

View file

@ -5,7 +5,7 @@
<div class="layout-col-1 menu">
<ul class="menu-items">
<li class="menu-item">
<nuxt-link :to="{ name: 'app' }" class="menu-link">
<nuxt-link :to="{ name: 'dashboard' }" class="menu-link">
<i class="fas fa-tachometer-alt"></i>
<span class="menu-link-text">Dashboard</span>
</nuxt-link>
@ -55,7 +55,7 @@
<div class="sidebar-content-wrapper">
<nav class="sidebar-content">
<div class="sidebar-title">
<img src="@/static/img/logo.svg" alt="" />
<img src="@baserow/modules/core/static/img/logo.svg" alt="" />
</div>
<Sidebar></Sidebar>
</nav>
@ -77,9 +77,9 @@
<script>
import { mapActions, mapGetters } from 'vuex'
import Notifications from '@/components/notifications/Notifications'
import GroupsContext from '@/components/group/GroupsContext'
import Sidebar from '@/components/sidebar/Sidebar'
import Notifications from '@baserow/modules/core/components/notifications/Notifications'
import GroupsContext from '@baserow/modules/core/components/group/GroupsContext'
import Sidebar from '@baserow/modules/core/components/sidebar/Sidebar'
export default {
// Application pages are pages that have the edit sidebar on the left side which

View file

@ -11,7 +11,7 @@
</template>
<script>
import Notifications from '@/components/notifications/Notifications'
import Notifications from '@baserow/modules/core/components/notifications/Notifications'
export default {
components: { Notifications }

View file

@ -0,0 +1,10 @@
import authentication from '@baserow/modules/core/middleware/authentication'
import authenticated from '@baserow/modules/core/middleware/authenticated'
import groupsAndApplications from '@baserow/modules/core/middleware/groupsAndApplications'
/* eslint-disable-next-line */
import Middleware from './middleware'
Middleware.authentication = authentication
Middleware.authenticated = authenticated
Middleware.groupsAndApplications = groupsAndApplications

View file

@ -8,6 +8,6 @@ export default function({ req, store, redirect }) {
// If the user is not authenticated we will redirect him to the login page.
if (!store.getters['auth/isAuthenticated']) {
redirect('/login')
redirect({ name: 'login' })
}
}

View file

@ -1,4 +1,4 @@
import { getToken } from '@/utils/auth'
import { getToken } from '@baserow/modules/core/utils/auth'
export default function({ store, req, app }) {
// If nuxt generate, pass this middleware

View file

@ -1,4 +1,4 @@
import { getGroupCookie } from '@/utils/group'
import { getGroupCookie } from '@baserow/modules/core/utils/group'
/**
* This middleware will make sure that all the groups and applications belonging to

View file

@ -0,0 +1,47 @@
/**
* This mixin works the best in combination with the Error component.
*/
export default {
data() {
return {
error: {
visible: false,
title: '',
message: ''
}
}
},
methods: {
/**
* Can be called after catching an error. If an handler is available the error
* data is populated with the correct error message.
*/
handleError(error, name, specificErrorMap = null) {
if (error.handler) {
const message = error.handler.getMessage(name, specificErrorMap)
this.showError(message)
error.handler.handled()
} else {
throw error
}
},
/**
* Populates the error data with the provided message. Can be called with an
* error message object or with a title and message.
*/
showError(title, message = null) {
this.error.visible = true
if (message === null) {
this.error.title = title.title
this.error.message = title.message
} else {
this.error.title = title
this.error.message = message
}
},
hideError() {
this.error.visible = false
}
}
}

View file

@ -0,0 +1,82 @@
import path from 'path'
import _ from 'lodash'
import serveStatic from 'serve-static'
import { routes } from './routes'
import head from './head'
export default function DatabaseModule(options) {
/**
* This function adds a plugin, but rather then prepending it to the list it will
* be appended.
*/
this.appendPlugin = template => {
this.addPlugin(template)
this.options.plugins.push(this.options.plugins.splice(0, 1)[0])
}
// Baserow must be run in universal mode.
this.options.mode = 'universal'
// Set the default head object, but override the configured head.
// @TODO if a child is a list the new children must be appended instead of overriden.
this.options.head = _.merge({}, head, this.options.head)
// Store must be true in order for the store to be injected into the context.
this.options.store = true
// Register new alias to the web-frontend directory.
this.options.alias['@baserow'] = path.resolve(__dirname, '../../')
// The core depends on these modules.
this.requireModule('@nuxtjs/axios')
this.requireModule('cookie-universal-nuxt')
// Serve the static directory
// @TODO we might need to change some things here for production. (See:
// https://github.com/nuxt/nuxt.js/blob/5a6cde3ebc23f04e89c30a4196a9b7d116b6d675/
// packages/server/src/server.js)
const staticMiddleware = serveStatic(
path.resolve(__dirname, 'static'),
this.options.render.static
)
this.addServerMiddleware(staticMiddleware)
// Add the layouts
this.addLayout(path.resolve(__dirname, 'layouts/app.vue'), 'app')
this.addLayout(path.resolve(__dirname, 'layouts/login.vue'), 'login')
const plugins = [
'middleware.js',
'plugin.js',
'plugins/auth.js',
'plugins/clientHandler.js',
'plugins/global.js',
'plugins/vuelidate.js'
]
plugins.forEach(plugin => {
this.addPlugin({
src: path.resolve(__dirname, plugin)
})
})
this.extendRoutes(configRoutes => {
// Remove all the routes created by nuxt.
let i = configRoutes.length
while (i--) {
if (configRoutes[i].component.indexOf('/@nuxt/') !== -1) {
configRoutes.splice(i, 1)
}
}
// Add the routes from the ./routes.js.
configRoutes.push(...routes)
})
// Add a default authentication middleware. In order to add a new middleware the
// middleware.js file has to be changed.
this.options.router.middleware.push('authentication')
// Add the main scss file which contains all the generic scss code.
this.options.css.push(path.resolve(__dirname, 'assets/scss/default.scss'))
}

View file

@ -35,6 +35,11 @@ import { mapState } from 'vuex'
export default {
layout: 'app',
head() {
return {
title: 'Dashboard'
}
},
computed: {
...mapState({
user: state => state.auth.user,

View file

@ -5,7 +5,7 @@
<script>
export default {
fetch({ store, redirect }) {
const name = store.getters['auth/isAuthenticated'] ? 'app' : 'login'
const name = store.getters['auth/isAuthenticated'] ? 'dashboard' : 'login'
redirect({ name: name })
}
}

View file

@ -1,15 +1,9 @@
<template>
<div>
<h1 class="box-title">
<img src="@/static/img/logo.svg" alt="" />
<img src="@baserow/modules/core/static/img/logo.svg" alt="" />
</h1>
<div v-if="error" class="alert alert-error alert-has-icon">
<div class="alert-icon">
<i class="fas fa-exclamation"></i>
</div>
<div class="alert-title">{{ errorTitle }}</div>
<p class="alert-content">{{ errorMessage }}</p>
</div>
<Error :error="error"></Error>
<form @submit.prevent="login">
<div class="control">
<label class="control-label">E-mail address</label>
@ -46,7 +40,7 @@
<div class="actions">
<ul class="action-links">
<li>
<nuxt-link :to="{ name: 'login-signup' }">
<nuxt-link :to="{ name: 'signup' }">
Sign up
</nuxt-link>
</li>
@ -66,9 +60,11 @@
<script>
import { required, email } from 'vuelidate/lib/validators'
import error from '@baserow/modules/core/mixins/error'
export default {
layout: 'login',
mixins: [error],
head() {
return {
title: 'Login'
@ -80,10 +76,7 @@ export default {
credentials: {
email: '',
password: ''
},
error: false,
errorTitle: '',
errorMessage: ''
}
}
},
validations: {
@ -100,33 +93,32 @@ export default {
}
this.loading = true
this.error = false
this.hideError()
try {
await this.$store.dispatch('auth/login', {
email: this.credentials.email,
password: this.credentials.password
})
this.$nuxt.$router.push({ name: 'app' })
this.$nuxt.$router.push({ name: 'dashboard' })
} catch (error) {
if (error.handler) {
const response = error.handler.response
// Because the API server does not yet respond with proper error codes we
// manually have to add the error here.
if (response && response.status === 400) {
this.errorTitle = 'Incorrect credentials'
this.errorMessage =
this.showError(
'Incorrect credentials',
'The provided e-mail address or password is ' + 'incorrect.'
)
this.credentials.password = ''
this.$v.$reset()
this.$refs.password.focus()
} else {
const message = error.handler.getMessage('login')
this.errorTitle = message.title
this.errorMessage = message.message
this.showError(message)
}
this.error = true
this.loading = false
error.handler.handled()
} else {

View file

@ -1,13 +1,7 @@
<template>
<div>
<h1 class="box-title">Sign up</h1>
<div v-if="error" class="alert alert-error alert-has-icon">
<div class="alert-icon">
<i class="fas fa-exclamation"></i>
</div>
<div class="alert-title">{{ errorTitle }}</div>
<p class="alert-content">{{ errorMessage }}</p>
</div>
<Error :error="error"></Error>
<form @submit.prevent="register">
<div class="control">
<label class="control-label">E-mail address</label>
@ -95,10 +89,12 @@
<script>
import { required, email, sameAs, minLength } from 'vuelidate/lib/validators'
import { ResponseErrorMessage } from '@/plugins/client'
import { ResponseErrorMessage } from '@baserow/modules/core/plugins/clientHandler'
import error from '@baserow/modules/core/mixins/error'
export default {
layout: 'login',
mixins: [error],
head() {
return {
title: 'Create new account'
@ -125,10 +121,7 @@ export default {
name: '',
password: '',
passwordConfirm: ''
},
error: false,
errorTitle: '',
errorMessage: ''
}
}
},
methods: {
@ -139,7 +132,7 @@ export default {
}
this.loading = true
this.error = false
this.hideError()
try {
await this.$store.dispatch('auth/register', {
@ -147,24 +140,15 @@ export default {
email: this.account.email,
password: this.account.password
})
this.$nuxt.$router.push({ name: 'app' })
this.$nuxt.$router.push({ name: 'dashboard' })
} catch (error) {
this.loading = false
if (error.handler) {
const message = error.handler.getMessage('signup', {
ERROR_EMAIL_ALREADY_EXISTS: new ResponseErrorMessage(
'User already exists.',
'A user with the provided e-mail address already exists.'
)
})
this.error = true
this.errorTitle = message.title
this.errorMessage = message.message
error.handler.handled()
} else {
throw error
}
this.handleError(error, 'signup', {
ERROR_EMAIL_ALREADY_EXISTS: new ResponseErrorMessage(
'User already exists.',
'A user with the provided e-mail address already exists.'
)
})
}
}
}

View file

@ -530,12 +530,17 @@
</template>
<script>
import Notifications from '@/components/notifications/Notifications'
import Notifications from '@baserow/modules/core/components/notifications/Notifications'
export default {
components: {
Notifications
},
head() {
return {
title: 'Style guide'
}
},
data() {
return {
checkbox: false,

View file

@ -0,0 +1,13 @@
import applicationStore from '@baserow/modules/core/store/application'
import authStore from '@baserow/modules/core/store/auth'
import groupStore from '@baserow/modules/core/store/group'
import notificationStore from '@baserow/modules/core/store/notification'
import sidebarStore from '@baserow/modules/core/store/sidebar'
export default ({ store }) => {
store.registerModule('application', applicationStore)
store.registerModule('auth', authStore)
store.registerModule('group', groupStore)
store.registerModule('notification', notificationStore)
store.registerModule('sidebar', sidebarStore)
}

View file

@ -1,5 +1,5 @@
import { client } from '@/services/client'
import { lowerCaseFirst } from '@/utils/string'
import { client } from '@baserow/modules/core/services/client'
import { lowerCaseFirst } from '@baserow/modules/core/utils/string'
export class ResponseErrorMessage {
constructor(title, message) {

View file

@ -0,0 +1,27 @@
import Vue from 'vue'
import Context from '@baserow/modules/core/components/Context'
import Modal from '@baserow/modules/core/components/Modal'
import Editable from '@baserow/modules/core/components/Editable'
import Dropdown from '@baserow/modules/core/components/Dropdown'
import DropdownItem from '@baserow/modules/core/components/DropdownItem'
import Checkbox from '@baserow/modules/core/components/Checkbox'
import Scrollbars from '@baserow/modules/core/components/Scrollbars'
import Error from '@baserow/modules/core/components/Error'
import lowercase from '@baserow/modules/core/filters/lowercase'
import scroll from '@baserow/modules/core/directives/scroll'
Vue.component('Context', Context)
Vue.component('Modal', Modal)
Vue.component('Editable', Editable)
Vue.component('Dropdown', Dropdown)
Vue.component('DropdownItem', DropdownItem)
Vue.component('Checkbox', Checkbox)
Vue.component('Scrollbars', Scrollbars)
Vue.component('Error', Error)
Vue.filter('lowercase', lowercase)
Vue.directive('scroll', scroll)

View file

@ -0,0 +1,29 @@
import path from 'path'
export const routes = [
{
name: 'index',
path: '',
component: path.resolve(__dirname, 'pages/index.vue')
},
{
name: 'login',
path: '/login',
component: path.resolve(__dirname, 'pages/login.vue')
},
{
name: 'signup',
path: '/signup',
component: path.resolve(__dirname, 'pages/signup.vue')
},
{
name: 'dashboard',
path: '/dashboard',
component: path.resolve(__dirname, 'pages/dashboard.vue')
},
{
name: 'style-guide',
path: '/style-guide',
component: path.resolve(__dirname, 'pages/style-guide.vue')
}
]

View file

Before

(image error) Size: 281 B

After

(image error) Size: 281 B

View file

Before

(image error) Size: 742 B

After

(image error) Size: 742 B

View file

Before

(image error) Size: 306 B

After

(image error) Size: 306 B

View file

Before

(image error) Size: 347 B

After

(image error) Size: 347 B

View file

Before

(image error) Size: 3.4 KiB

After

(image error) Size: 3.4 KiB

View file

@ -1,6 +1,6 @@
import { ApplicationType } from '@/core/applicationTypes'
import ApplicationService from '@/services/application'
import { clone } from '@/utils/object'
import { ApplicationType } from '@baserow/modules/core/applicationTypes'
import ApplicationService from '@baserow/modules/core/services/application'
import { clone } from '@baserow/modules/core/utils/object'
function populateApplication(application, getters) {
const type = getters.getType(application.type)
@ -87,24 +87,23 @@ export const actions = {
/**
* Fetches all the application of the authenticated user.
*/
fetchAll({ commit, getters }) {
async fetchAll({ commit, getters }) {
commit('SET_LOADING', true)
return ApplicationService.fetchAll()
.then(({ data }) => {
data.forEach((part, index, d) => {
populateApplication(data[index], getters)
})
commit('SET_ITEMS', data)
commit('SET_LOADING', false)
commit('SET_LOADED', true)
try {
const { data } = await ApplicationService.fetchAll()
data.forEach((part, index, d) => {
populateApplication(data[index], getters)
})
.catch(error => {
commit('SET_ITEMS', [])
commit('SET_LOADING', false)
commit('SET_ITEMS', data)
commit('SET_LOADING', false)
commit('SET_LOADED', true)
} catch (error) {
commit('SET_ITEMS', [])
commit('SET_LOADING', false)
throw error
})
throw error
}
},
/**
* Clears all the currently selected applications, this could be called when
@ -129,7 +128,7 @@ export const actions = {
* Creates a new application with the given type and values for the currently
* selected group.
*/
create({ commit, getters, rootGetters, dispatch }, { type, values }) {
async create({ commit, getters, rootGetters, dispatch }, { type, values }) {
if (values.hasOwnProperty('type')) {
throw new Error(
'The key "type" is a reserved, but is already set on the ' +
@ -141,48 +140,44 @@ export const actions = {
throw new Error(`An application type with type "${type}" doesn't exist.`)
}
const data = clone(values)
data.type = type
return ApplicationService.create(
const postData = clone(values)
postData.type = type
const { data } = await ApplicationService.create(
rootGetters['group/selectedId'],
data
).then(({ data }) => {
populateApplication(data, getters)
commit('ADD_ITEM', data)
})
postData
)
populateApplication(data, getters)
commit('ADD_ITEM', data)
},
/**
* Updates the values of an existing application.
*/
update({ commit, dispatch, getters }, { application, values }) {
return ApplicationService.update(application.id, values).then(
({ data }) => {
// Create a dict with only the values we want to update.
const update = Object.keys(values).reduce((result, key) => {
result[key] = data[key]
return result
}, {})
commit('UPDATE_ITEM', { id: application.id, values: update })
}
)
async update({ commit, dispatch, getters }, { application, values }) {
const { data } = await ApplicationService.update(application.id, values)
// Create a dict with only the values we want to update.
const update = Object.keys(values).reduce((result, key) => {
result[key] = data[key]
return result
}, {})
commit('UPDATE_ITEM', { id: application.id, values: update })
},
/**
* Deletes an existing application.
*/
delete({ commit, dispatch, getters }, application) {
return ApplicationService.delete(application.id)
.then(() => {
const type = getters.getType(application.type)
type.delete(application, this)
async delete({ commit, dispatch, getters }, application) {
try {
await ApplicationService.delete(application.id)
const type = getters.getType(application.type)
type.delete(application, this)
commit('DELETE_ITEM', application.id)
} catch (error) {
if (error.response && error.response.status === 404) {
commit('DELETE_ITEM', application.id)
})
.catch(error => {
if (error.response && error.response.status === 404) {
commit('DELETE_ITEM', application.id)
} else {
throw error
}
})
} else {
throw error
}
}
},
/**
* Select an application.
@ -232,3 +227,11 @@ export const getters = {
return state.types[type]
}
}
export default {
namespaced: true,
state,
getters,
actions,
mutations
}

View file

@ -1,8 +1,8 @@
import jwtDecode from 'jwt-decode'
import AuthService from '@/services/auth'
import { setToken, unsetToken } from '@/utils/auth'
import { unsetGroupCookie } from '@/utils/group'
import AuthService from '@baserow/modules/core/services/auth'
import { setToken, unsetToken } from '@baserow/modules/core/utils/auth'
import { unsetGroupCookie } from '@baserow/modules/core/utils/group'
export const state = () => ({
refreshing: false,
@ -30,58 +30,53 @@ export const actions = {
* Authenticate a user by his email and password. If successful commit the
* token to the state and start the refresh timeout to stay authenticated.
*/
login({ commit, dispatch }, { email, password }) {
return AuthService.login(email, password).then(({ data }) => {
setToken(data.token, this.app.$cookies)
commit('SET_USER_DATA', data)
dispatch('startRefreshTimeout')
})
async login({ commit, dispatch }, { email, password }) {
const { data } = await AuthService.login(email, password)
setToken(data.token, this.app.$cookies)
commit('SET_USER_DATA', data)
dispatch('startRefreshTimeout')
},
/**
* Register a new user and immediately authenticate. If successful commit the
* token to the state and start the refresh timeout to stay authenticated.
*/
register({ commit, dispatch }, { email, name, password }) {
return AuthService.register(email, name, password, true).then(
({ data }) => {
setToken(data.token, this.app.$cookies)
commit('SET_USER_DATA', data)
dispatch('startRefreshTimeout')
}
)
async register({ commit, dispatch }, { email, name, password }) {
const { data } = await AuthService.register(email, name, password, true)
setToken(data.token, this.app.$cookies)
commit('SET_USER_DATA', data)
dispatch('startRefreshTimeout')
},
/**
* Logs off the user by removing the token as a cookie and clearing the user
* data.
*/
logoff({ commit, dispatch }) {
async logoff({ commit, dispatch }) {
unsetToken(this.app.$cookies)
unsetGroupCookie(this.app.$cookies)
commit('CLEAR_USER_DATA')
dispatch('group/clearAll', {}, { root: true })
dispatch('group/unselect', {}, { root: true })
await dispatch('group/clearAll', {}, { root: true })
await dispatch('group/unselect', {}, { root: true })
},
/**
* Refresh the existing token. If successful commit the new token and start a
* new refresh timeout. If unsuccessful the existing cookie and user data is
* cleared.
*/
refresh({ commit, state, dispatch }, token) {
return AuthService.refresh(token)
.then(({ data }) => {
setToken(data.token, this.app.$cookies)
commit('SET_USER_DATA', data)
dispatch('startRefreshTimeout')
})
.catch(() => {
// The token could not be refreshed, this means the token is no longer
// valid and the user not logged in anymore.
unsetToken(this.app.$cookies)
commit('CLEAR_USER_DATA')
async refresh({ commit, state, dispatch }, token) {
try {
const { data } = await AuthService.refresh(token)
setToken(data.token, this.app.$cookies)
commit('SET_USER_DATA', data)
dispatch('startRefreshTimeout')
} catch {
// The token could not be refreshed, this means the token is no longer
// valid and the user not logged in anymore.
unsetToken(this.app.$cookies)
commit('CLEAR_USER_DATA')
// @TODO we might want to do something here, trigger some event, show
// show the user a login popup or redirect to the login page.
})
// @TODO we might want to do something here, trigger some event, show
// show the user a login popup or redirect to the login page.
}
},
/**
* Because the token expires within a configurable time, we need to keep
@ -131,3 +126,11 @@ export const getters = {
return state.token_data.exp - now
}
}
export default {
namespaced: true,
state,
getters,
actions,
mutations
}

View file

@ -1,5 +1,8 @@
import GroupService from '@/services/group'
import { setGroupCookie, unsetGroupCookie } from '@/utils/group'
import GroupService from '@baserow/modules/core/services/group'
import {
setGroupCookie,
unsetGroupCookie
} from '@baserow/modules/core/utils/group'
function populateGroup(group) {
group._ = { loading: false, selected: false }
@ -88,59 +91,54 @@ export const actions = {
/**
* Fetches all the groups of an authenticated user.
*/
fetchAll({ commit }) {
async fetchAll({ commit }) {
commit('SET_LOADING', true)
return GroupService.fetchAll()
.then(({ data }) => {
commit('SET_LOADED', true)
commit('SET_ITEMS', data)
})
.catch(() => {
commit('SET_ITEMS', [])
})
.then(() => {
commit('SET_LOADING', false)
})
try {
const { data } = await GroupService.fetchAll()
commit('SET_LOADED', true)
commit('SET_ITEMS', data)
} catch {
commit('SET_ITEMS', [])
}
commit('SET_LOADING', false)
},
/**
* Creates a new group with the given values.
*/
create({ commit }, values) {
return GroupService.create(values).then(({ data }) => {
commit('ADD_ITEM', data)
})
async create({ commit }, values) {
const { data } = await GroupService.create(values)
commit('ADD_ITEM', data)
},
/**
* Updates the values of the group with the provided id.
*/
update({ commit, dispatch }, { group, values }) {
return GroupService.update(group.id, values).then(({ data }) => {
// Create a dict with only the values we want to update.
const update = Object.keys(values).reduce((result, key) => {
result[key] = data[key]
return result
}, {})
commit('UPDATE_ITEM', { id: group.id, values: update })
})
async update({ commit, dispatch }, { group, values }) {
const { data } = await GroupService.update(group.id, values)
// Create a dict with only the values we want to update.
const update = Object.keys(values).reduce((result, key) => {
result[key] = data[key]
return result
}, {})
commit('UPDATE_ITEM', { id: group.id, values: update })
},
/**
* Deletes an existing group with the provided id.
*/
delete({ commit, dispatch }, group) {
return GroupService.delete(group.id)
.then(() => {
dispatch('forceDelete', group)
})
.catch(error => {
// If the group to delete wasn't found we can just delete it from the
// state.
if (error.response && error.response.status === 404) {
dispatch('forceDelete', group)
} else {
throw error
}
})
async delete({ commit, dispatch }, group) {
try {
await GroupService.delete(group.id)
await dispatch('forceDelete', group)
} catch (error) {
// If the group to delete wasn't found we can just delete it from the
// state.
if (error.response && error.response.status === 404) {
await dispatch('forceDelete', group)
} else {
throw error
}
}
},
/**
* Forcibly remove the group from the items without calling the server.
@ -200,3 +198,11 @@ export const getters = {
return state.selected.id
}
}
export default {
namespaced: true,
state,
getters,
actions,
mutations
}

View file

@ -1,4 +1,4 @@
import { uuid } from '@/utils/string'
import { uuid } from '@baserow/modules/core/utils/string'
export const state = () => ({
items: []
@ -44,3 +44,11 @@ export const actions = {
}
export const getters = {}
export default {
namespaced: true,
state,
getters,
actions,
mutations
}

View file

@ -22,3 +22,11 @@ export const getters = {
return !!state.collapsed
}
}
export default {
namespaced: true,
state,
getters,
actions,
mutations
}

Some files were not shown because too many files have changed in this diff Show more