mirror of https://github.com/doccano/doccano.git
Hironsan
5 years ago
7 changed files with 232 additions and 290 deletions
Split View
Diff Options
-
3frontend/components/Modal.vue
-
17frontend/layouts/projects.vue
-
426frontend/pages/projects/index.vue
-
5frontend/services/api.service.js
-
21frontend/services/project.service.js
-
46frontend/test/project.service.spec.js
-
4frontend/test/request.spec.js
@ -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> |
@ -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() |
|||
} |
|||
} |
|||
} |
|||
|
@ -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 |
|||
}) |
@ -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() |
@ -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({}) |
|||
}) |
|||
}) |
Write
Preview
Loading…
Cancel
Save