Browse Source

Add project service

pull/341/head
Hironsan 5 years ago
parent
commit
7a6167a23f
7 changed files with 232 additions and 290 deletions
  1. 3
      frontend/components/Modal.vue
  2. 17
      frontend/layouts/projects.vue
  3. 426
      frontend/pages/projects/index.vue
  4. 5
      frontend/services/api.service.js
  5. 21
      frontend/services/project.service.js
  6. 46
      frontend/test/project.service.spec.js
  7. 4
      frontend/test/request.spec.js

3
frontend/components/Modal.vue

@ -29,7 +29,7 @@
<v-btn
class="text-none"
text
@click="dialog = false"
@click="agree"
>
{{ button }}
</v-btn>
@ -59,6 +59,7 @@ export default {
},
agree() {
this.dialog = false
this.$emit('agree')
},
cancel() {
this.dialog = false

17
frontend/layouts/projects.vue

@ -0,0 +1,17 @@
<template>
<base-layout>
<template #content>
<nuxt />
</template>
</base-layout>
</template>
<script>
import BaseLayout from '~/layouts/BaseLayout'
export default {
components: {
BaseLayout
}
}
</script>

426
frontend/pages/projects/index.vue

@ -1,333 +1,189 @@
<template>
<v-content>
<v-app-bar
:clipped-left="$vuetify.breakpoint.lgAndUp"
app
color="blue darken-3"
dark
dense
>
<v-toolbar-title
style="width: 300px"
class="ml-0 pl-4"
>
<v-app-bar-nav-icon @click.stop="drawer = !drawer" />
<span class="hidden-sm-and-down">doccano</span>
</v-toolbar-title>
<v-text-field
flat
solo-inverted
hide-details
prepend-inner-icon="search"
label="Search"
class="hidden-sm-and-down"
/>
<v-spacer />
<v-btn icon>
<v-icon>apps</v-icon>
</v-btn>
<v-btn icon>
<v-icon>notifications</v-icon>
</v-btn>
<v-btn
icon
large
>
<v-avatar
size="32px"
item
>
<v-img
src="https://cdn.vuetifyjs.com/images/logos/logo.svg"
alt="Vuetify"
/>
</v-avatar>
</v-btn>
</v-app-bar>
<v-container
fluid
fill-height
>
<v-layout
align-center
justify-center
>
<v-flex>
<v-card>
<v-card-title>
Projects
<v-spacer />
<v-text-field
v-model="search"
append-icon="search"
label="Search"
single-line
hide-details
/>
<v-btn
class="mb-2 text-capitalize"
color="primary"
@click="openAddModal"
>
Add Project
</v-btn>
<Modal
ref="childDialogue"
:title="addModal.title"
:button="addModal.button"
@agree="createProject"
>
<v-form
ref="form"
v-model="valid"
lazy-validation
>
<v-text-field
v-model="newProject.name"
:rules="nameRules"
label="Project name"
prepend-icon="label"
required
autofocus
/>
<v-text-field
v-model="newProject.description"
:rules="nameRules"
label="Description"
prepend-icon="label"
required
/>
<v-select
v-model="newProject.project_type"
:items="projectTypes"
:rules="[v => !!v || 'Type is required']"
label="projectType"
prepend-icon="mdi-keyboard"
required
/>
</v-form>
</Modal>
<v-btn
class="mb-2 ml-2 text-capitalize"
outlined
:disabled="selected.length === 0"
@click="openRemoveModal"
>
Remove
</v-btn>
<Modal
ref="removeDialogue"
:title="removeModal.title"
:button="removeModal.button"
@agree="deleteProject"
>
Are you sure you want to remove these projects?
<v-list dense>
<v-list-item v-for="(item, i) in selected" :key="i">
<v-list-item-content>
<v-list-item-title>{{ item.name }}</v-list-item-title>
</v-list-item-content>
</v-list-item>
</v-list>
</Modal>
</v-card-title>
<v-data-table
v-model="selected"
:headers="headers"
:items="desserts"
:items="projects"
:search="search"
item-key="id"
show-select
>
<template v-slot:item.calories="{ item }">
<v-chip :color="getColor(item.calories)" dark>
{{ item.calories }}
</v-chip>
<template v-slot:top>
<v-text-field
v-model="search"
prepend-inner-icon="search"
label="Search"
single-line
hide-details
filled
/>
</template>
</v-data-table>
</v-card>
</v-flex>
</v-layout>
</v-container>
<v-btn
bottom
color="pink"
dark
fab
fixed
right
@click="dialog = !dialog"
>
<v-icon>add</v-icon>
</v-btn>
<v-dialog
v-model="dialog"
width="800px"
>
<v-card>
<v-card-title class="grey darken-2">
Create contact
</v-card-title>
<v-container grid-list-sm>
<v-layout
wrap
>
<v-flex
xs12
align-center
justify-space-between
>
<v-layout align-center>
<v-avatar
size="40px"
class="mr-4"
>
<img
src="//ssl.gstatic.com/s2/oz/images/sge/grey_silhouette.png"
alt=""
>
</v-avatar>
<v-text-field
placeholder="Name"
/>
</v-layout>
</v-flex>
<v-flex xs6>
<v-text-field
prepend-icon="business"
placeholder="Company"
/>
</v-flex>
<v-flex xs6>
<v-text-field
placeholder="Job title"
/>
</v-flex>
<v-flex xs12>
<v-text-field
prepend-icon="mail"
placeholder="Email"
/>
</v-flex>
<v-flex xs12>
<v-text-field
type="tel"
prepend-icon="phone"
placeholder="(000) 000 - 0000"
/>
</v-flex>
<v-flex xs12>
<v-text-field
prepend-icon="notes"
placeholder="Notes"
/>
</v-flex>
</v-layout>
</v-container>
<v-card-actions>
<v-btn
text
color="primary"
>
More
</v-btn>
<v-spacer />
<v-btn
text
color="primary"
@click="dialog = false"
>
Cancel
</v-btn>
<v-btn
text
@click="dialog = false"
>
Save
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-content>
</template>
<script>
import Modal from '~/components/Modal'
import ProjectService from '~/services/project.service'
export default {
layout: 'projectList',
props: {
source: {
type: String,
default: 'cat'
}
layout: 'projects',
components: {
Modal
},
data: () => ({
dialog: false,
drawer: null,
items: [
{ icon: 'contacts', text: 'Contacts' },
{ icon: 'history', text: 'Frequently contacted' },
{ icon: 'content_copy', text: 'Duplicates' },
{
icon: 'keyboard_arrow_up',
'icon-alt': 'keyboard_arrow_down',
text: 'Labels',
model: true,
children: [
{ icon: 'add', text: 'Create label' }
]
},
{
icon: 'keyboard_arrow_up',
'icon-alt': 'keyboard_arrow_down',
text: 'More',
model: false,
children: [
{ text: 'Import' },
{ text: 'Export' },
{ text: 'Print' },
{ text: 'Undo changes' },
{ text: 'Other contacts' }
]
},
{ icon: 'settings', text: 'Settings' },
{ icon: 'chat_bubble', text: 'Send feedback' },
{ icon: 'help', text: 'Help' },
{ icon: 'phonelink', text: 'App downloads' },
{ icon: 'keyboard', text: 'Go to the old version' }
],
valid: true,
search: '',
selected: [],
selectedUser: null,
projectTypes: [
'Text Classification',
'Sequence Labeling',
'Sequence to sequence'
], // Todo: Get project types from backend server.
projects: [],
newProject: {
name: '',
description: '',
project_type: null
},
addModal: {
title: 'Add Project',
button: 'Add Project'
},
removeModal: {
title: 'Remove Project',
button: 'Yes, remove'
},
headers: [
{
text: 'Name',
align: 'left',
sortable: false,
value: 'name'
},
{ text: 'calories', value: 'calories' },
{ text: 'Description', value: 'fat' }
// { text: 'Carbs (g)', value: 'carbs' },
// { text: 'Protein (g)', value: 'protein' },
// { text: 'Iron (%)', value: 'iron' }
],
desserts: [
{
name: 'Frozen Yogurt',
calories: 159,
fat: 6.0,
carbs: 24,
protein: 4.0,
iron: '1%'
},
{
name: 'Ice cream sandwich',
calories: 237,
fat: 9.0,
carbs: 37,
protein: 4.3,
iron: '1%'
},
{
name: 'Eclair',
calories: 262,
fat: 16.0,
carbs: 23,
protein: 6.0,
iron: '7%'
},
{
name: 'Cupcake',
calories: 305,
fat: 3.7,
carbs: 67,
protein: 4.3,
iron: '8%'
},
{
name: 'Gingerbread',
calories: 356,
fat: 16.0,
carbs: 49,
protein: 3.9,
iron: '16%'
},
{
name: 'Jelly bean',
calories: 375,
fat: 0.0,
carbs: 94,
protein: 0.0,
iron: '0%'
},
{
name: 'Lollipop',
calories: 392,
fat: 0.2,
carbs: 98,
protein: 0,
iron: '2%'
text: 'Description',
value: 'description'
},
{
name: 'Honeycomb',
calories: 408,
fat: 3.2,
carbs: 87,
protein: 6.5,
iron: '45%'
},
{
name: 'Donut',
calories: 452,
fat: 25.0,
carbs: 51,
protein: 4.9,
iron: '22%'
},
{
name: 'KitKat',
calories: 518,
fat: 26.0,
carbs: 65,
protein: 7,
iron: '6%'
text: 'Type',
value: 'project_type'
}
],
nameRules: [
v => !!v || 'Name is required'
]
}),
async created() {
this.projects = await ProjectService.getProjectList()
},
methods: {
getColor(calories) {
if (calories > 400) return 'red'
else if (calories > 200) return 'orange'
else return 'green'
async createProject() {
const response = await ProjectService.createProject(this.newProject)
this.projects.unshift(response)
this.newProject = {
name: '',
description: '',
project_type: null
}
},
async deleteProject() {
// Todo: bulk delete.
for (const project of this.selected) {
await ProjectService.deleteProject(project.id)
this.projects = this.projects.filter(item => item.id !== project.id)
}
this.selected = []
},
openAddModal() {
this.$refs.childDialogue.open()
},
openRemoveModal() {
this.$refs.removeDialogue.open()
}
}
}

frontend/services/request.js → frontend/services/api.service.js

@ -1,7 +1,8 @@
import axios from 'axios'
const baseURL = 'http://localhost:3000/v1' // Todo: change URL by development/staging/production.
export default class Request {
constructor(baseURL) {
export default class ApiService {
constructor() {
this.instance = axios.create({
baseURL
})

21
frontend/services/project.service.js

@ -0,0 +1,21 @@
import ApiService from '@/services/api.service'
class ProjectService {
constructor() {
this.request = new ApiService()
}
getProjectList() {
return this.request.get('/projects')
}
createProject(data) {
return this.request.post('/projects', data)
}
deleteProject(projectId) {
return this.request.delete(`/projects/${projectId}`)
}
}
export default new ProjectService()

46
frontend/test/project.service.spec.js

@ -0,0 +1,46 @@
import MockAdapter from 'axios-mock-adapter'
import ProjectService from '@/services/project.service.js'
describe('Project.service', () => {
const mockAxios = new MockAdapter(ProjectService.request.instance)
test('can get project list', async () => {
const data = [
{
id: 1,
name: 'CoNLL 2003',
description: 'This is a project for NER.',
guideline: 'Please write annotation guideline.',
users: [1],
project_type: 'SequenceLabeling',
image: '/static/assets/images/cats/sequence_labeling.jpg',
updated_at: '2019-07-09T06:19:29.789091Z',
randomize_document_order: false,
resourcetype: 'SequenceLabelingProject'
}
]
mockAxios.onGet('/projects').reply(200, data)
const response = await ProjectService.getProjectList()
expect(response).toEqual(data)
})
test('can create a project', async () => {
const data = {
name: 'test project',
description: 'test description',
guideline: 'Please write annotation guideline.',
project_type: 'SequenceLabeling',
randomize_document_order: false
}
mockAxios.onPost('/projects').reply(201, data)
const response = await ProjectService.createProject(data)
expect(response.title).toEqual(data.title)
})
test('can delete a project', async () => {
const projectId = 1
mockAxios.onDelete(`/projects/${projectId}`).reply(204, {})
const response = await ProjectService.deleteProject(projectId)
expect(response).toEqual({})
})
})

4
frontend/test/request.spec.js

@ -1,8 +1,8 @@
import MockAdapter from 'axios-mock-adapter'
import Request from '@/services/request.js'
import ApiService from '@/services/api.service'
describe('Request', () => {
const r = new Request('')
const r = new ApiService('')
const mockAxios = new MockAdapter(r.instance)
test('can get resources', async () => {

Loading…
Cancel
Save