Browse Source

Add frontend boilerplate

pull/341/head
Hironsan 5 years ago
parent
commit
18b859b3c0
52 changed files with 13149 additions and 0 deletions
  1. 16
      frontend/.babelrc
  2. 17
      frontend/.eslintrc.js
  3. 88
      frontend/.gitignore
  4. 22
      frontend/README.md
  5. 7
      frontend/assets/README.md
  6. 2
      frontend/assets/style/app.styl
  7. 1
      frontend/assets/style/variables.styl
  8. 90
      frontend/components/EntityItem.vue
  9. 113
      frontend/components/EntityItemContainer.vue
  10. 79
      frontend/components/Logo.vue
  11. 68
      frontend/components/Modal.vue
  12. 7
      frontend/components/README.md
  13. 21
      frontend/components/VuetifyLogo.vue
  14. 85
      frontend/components/project/SideBarLeft.vue
  15. 208
      frontend/components/project/SideBarRight/SideBarLabeling.vue
  16. 17
      frontend/jest.config.js
  17. 38
      frontend/layouts/BaseLayout.vue
  18. 7
      frontend/layouts/README.md
  19. 61
      frontend/layouts/annotation.vue
  20. 50
      frontend/layouts/default.vue
  21. 44
      frontend/layouts/error.vue
  22. 37
      frontend/layouts/project.vue
  23. 10
      frontend/layouts/projectList.vue
  24. 8
      frontend/middleware/README.md
  25. 90
      frontend/nuxt.config.js
  26. 47
      frontend/package.json
  27. 6
      frontend/pages/README.md
  28. 116
      frontend/pages/index.vue
  29. 23
      frontend/pages/inspire.vue
  30. 205
      frontend/pages/projects/_id/annotation/index.vue
  31. 142
      frontend/pages/projects/_id/dataset/index.vue
  32. 9
      frontend/pages/projects/_id/download/index.vue
  33. 50
      frontend/pages/projects/_id/guideline/index.vue
  34. 116
      frontend/pages/projects/_id/index.vue
  35. 253
      frontend/pages/projects/_id/labels/index.vue
  36. 9
      frontend/pages/projects/_id/statistics/index.vue
  37. 120
      frontend/pages/projects/_id/upload/index.vue
  38. 218
      frontend/pages/projects/_id/users/index.vue
  39. 334
      frontend/pages/projects/index.vue
  40. 7
      frontend/plugins/README.md
  41. 15
      frontend/plugins/filters.js
  42. 8
      frontend/plugins/utils.js
  43. 11
      frontend/static/README.md
  44. BIN
      frontend/static/favicon.ico
  45. BIN
      frontend/static/images/hero.jpeg
  46. BIN
      frontend/static/images/vuetify.png
  47. BIN
      frontend/static/v.png
  48. 10
      frontend/store/README.md
  49. 9
      frontend/store/sidebar.js
  50. 55
      frontend/test/Modal.spec.js
  51. 18
      frontend/test/filters.spec.js
  52. 10182
      frontend/yarn.lock

16
frontend/.babelrc

@ -0,0 +1,16 @@
{
"env": {
"test": {
"presets": [
[
"@babel/preset-env",
{
"targets": {
"node": "current"
}
}
]
]
}
}
}

17
frontend/.eslintrc.js

@ -0,0 +1,17 @@
module.exports = {
root: true,
env: {
browser: true,
node: true
},
parserOptions: {
parser: 'babel-eslint'
},
extends: [
'@nuxtjs',
'plugin:nuxt/recommended'
],
// add your custom rules here
rules: {
}
}

88
frontend/.gitignore

@ -0,0 +1,88 @@
# Created by .ignore support plugin (hsz.mobi)
### Node template
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# parcel-bundler cache (https://parceljs.org/)
.cache
# next.js build output
.next
# nuxt.js build output
.nuxt
# Nuxt generate
dist
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless
# IDE / Editor
.idea
.editorconfig
# Service worker
sw.*
# Mac OSX
.DS_Store

22
frontend/README.md

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

7
frontend/assets/README.md

@ -0,0 +1,7 @@
# ASSETS
**This directory is not required, you can delete it if you don't want to use it.**
This directory contains your un-compiled assets such as LESS, SASS, or JavaScript.
More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/assets#webpacked).

2
frontend/assets/style/app.styl

@ -0,0 +1,2 @@
// Import Vuetify styling
@require '~vuetify/src/stylus/app.styl'

1
frontend/assets/style/variables.styl

@ -0,0 +1 @@
@require '~vuetify/src/stylus/settings/_variables.styl'

90
frontend/components/EntityItem.vue

@ -0,0 +1,90 @@
<template>
<span v-if="label" class="highlight bottom" :style="{ borderColor: color }">
<span class="highlight__content">{{ content }}</span><span class="highlight__label" :data-label="label" :style="{backgroundColor: color}" @click="open" />
</span>
<span v-else>{{ content }}</span>
</template>
<script>
import { idealColor } from '~/plugins/utils.js'
export default {
props: {
content: {
type: String,
default: '',
required: true
},
label: {
type: String,
default: ''
},
color: {
type: String,
default: '#64FFDA'
}
},
computed: {
textColor() {
return idealColor(this.color)
}
},
methods: {
open() {
alert('hello')
}
}
}
</script>
<style scoped>
.highlight.blue {
background: #edf4fa !important;
}
.highlight.bottom {
display: block;
white-space: normal;
}
.highlight:first-child {
margin-left: 0;
}
.highlight {
border: 2px solid;
color: #232323;
margin: 4px 6px 4px 3px;
vertical-align: middle;
box-shadow: 2px 4px 20px rgba(0,0,0,.1);
position: relative;
cursor: default;
min-width: 26px;
line-height: 22px;
display: flex;
}
.highlight__content {
display: flex;
flex-wrap: wrap;
align-items: center;
padding: 2px 2px 0px 6px;
}
.highlight.bottom .highlight__content:after {
content: " ";
padding-right: 3px;
}
.highlight__label {
line-height: 14px;
padding-top: 1px;
align-items: center;
justify-content: center;
display: flex;
padding: 0 8px;
text-align: center;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
color: white;
}
.highlight__label::after {
content: attr(data-label);
}
</style>

113
frontend/components/EntityItemContainer.vue

@ -0,0 +1,113 @@
<template>
<div class="highlight-container highlight-container--bottom-labels" @mouseup="open">
<entity-item v-for="(chunk, i) in chunks" :key="i" :content="chunk.text" :label="chunk.label" :color="chunk.color" />
</div>
</template>
<script>
import EntityItem from '~/components/EntityItem'
export default {
components: {
EntityItem
},
props: {
content: {
type: String,
default: '',
required: true
},
labels: {
type: Array,
default: () => ([]),
required: true
},
entities: {
type: Array,
default: () => ([]),
required: true
}
},
computed: {
sortedEntities() {
return this.entities.slice().sort((a, b) => a.start_offset - b.start_offset)
},
chunks() {
const chunks = []
const entities = this.sortedEntities
let startOffset = 0
for (const entity of entities) {
// add non-entities to chunks.
chunks.push({
label: null,
color: null,
text: this.content.slice(startOffset, entity.start_offset)
})
startOffset = entity.end_offset
// add entities to chunks.
const label = this.labelObject[entity.label]
chunks.push({
label: label.name,
color: label.color,
text: this.content.slice(entity.start_offset, entity.end_offset)
})
}
// add the rest of text.
chunks.push({
label: null,
color: null,
text: this.content.slice(startOffset, this.content.length)
})
return chunks
},
labelObject() {
const obj = {}
for (const label of this.labels) {
obj[label.id] = label
}
return obj
}
},
methods: {
open() {
let selection
// Modern browsers.
if (window.getSelection) {
selection = window.getSelection()
} else if (document.selection) {
selection = document.selection
}
// If something is selected.
if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0)
const preSelectionRange = range.cloneRange()
preSelectionRange.selectNodeContents(this.$el)
preSelectionRange.setEnd(range.startContainer, range.startOffset)
const start = [...preSelectionRange.toString()].length
const end = start + [...range.toString()].length
alert(start + ' ' + end)
return end
}
}
}
}
</script>
<style scoped>
.highlight-container.highlight-container--bottom-labels {
align-items: flex-start;
}
.highlight-container {
line-height: 42px!important;
display: flex;
flex-wrap: wrap;
white-space: pre-wrap;
cursor: default;
}
.highlight-container.highlight-container--bottom-labels .highlight.bottom {
margin-top: 6px;
}
</style>

79
frontend/components/Logo.vue

@ -0,0 +1,79 @@
<template>
<div class="VueToNuxtLogo">
<div class="Triangle Triangle--two" />
<div class="Triangle Triangle--one" />
<div class="Triangle Triangle--three" />
<div class="Triangle Triangle--four" />
</div>
</template>
<style>
.VueToNuxtLogo {
display: inline-block;
animation: turn 2s linear forwards 1s;
transform: rotateX(180deg);
position: relative;
overflow: hidden;
height: 180px;
width: 245px;
}
.Triangle {
position: absolute;
top: 0;
left: 0;
width: 0;
height: 0;
}
.Triangle--one {
border-left: 105px solid transparent;
border-right: 105px solid transparent;
border-bottom: 180px solid #41b883;
}
.Triangle--two {
top: 30px;
left: 35px;
animation: goright 0.5s linear forwards 3.5s;
border-left: 87.5px solid transparent;
border-right: 87.5px solid transparent;
border-bottom: 150px solid #3b8070;
}
.Triangle--three {
top: 60px;
left: 35px;
animation: goright 0.5s linear forwards 3.5s;
border-left: 70px solid transparent;
border-right: 70px solid transparent;
border-bottom: 120px solid #35495e;
}
.Triangle--four {
top: 120px;
left: 70px;
animation: godown 0.5s linear forwards 3s;
border-left: 35px solid transparent;
border-right: 35px solid transparent;
border-bottom: 60px solid #fff;
}
@keyframes turn {
100% {
transform: rotateX(0deg);
}
}
@keyframes godown {
100% {
top: 180px;
}
}
@keyframes goright {
100% {
left: 70px;
}
}
</style>

68
frontend/components/Modal.vue

@ -0,0 +1,68 @@
<template>
<v-dialog
v-model="dialog"
width="800px"
>
<v-card>
<v-card-title class="grey lighten-2">
{{ title }}
</v-card-title>
<v-container grid-list-sm>
<v-layout
wrap
>
<v-flex xs12>
<slot />
</v-flex>
</v-layout>
</v-container>
<v-card-actions>
<v-spacer />
<v-btn
class="text-capitalize"
text
color="primary"
@click="dialog = false"
>
Cancel
</v-btn>
<v-btn
class="text-none"
text
@click="dialog = false"
>
{{ button }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script>
export default {
props: {
title: {
type: String,
default: ''
},
button: {
type: String,
default: 'Yes'
}
},
data: () => ({
dialog: false
}),
methods: {
open() {
this.dialog = true
},
agree() {
this.dialog = false
},
cancel() {
this.dialog = false
}
}
}
</script>

7
frontend/components/README.md

@ -0,0 +1,7 @@
# COMPONENTS
**This directory is not required, you can delete it if you don't want to use it.**
The components directory contains your Vue.js Components.
_Nuxt.js doesn't supercharge these components._

21
frontend/components/VuetifyLogo.vue

@ -0,0 +1,21 @@
<template>
<img
class="VuetifyLogo"
alt="Vuetify Logo"
src="data:image/png;base64,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"
>
</template>
<style>
.VuetifyLogo {
width: 180px;
transform: rotateY(560deg);
animation: turn 3.5s ease-out forwards 1s;
}
@keyframes turn {
100% {
transform: rotateY(0deg);
}
}
</style>

85
frontend/components/project/SideBarLeft.vue

@ -0,0 +1,85 @@
<template>
<v-list
dense
class=""
>
<v-list-item>
<v-btn rounded color="white">
<v-icon left>
mdi-plus
</v-icon> Start Labeling
</v-btn>
</v-list-item>
<template v-for="(item, i) in items">
<v-layout
v-if="item.heading"
:key="i"
align-center
>
<v-flex xs6>
<v-subheader v-if="item.heading">
{{ item.heading }}
</v-subheader>
</v-flex>
<v-flex
xs6
class="text-right"
>
<v-btn
small
text
>
edit
</v-btn>
</v-flex>
</v-layout>
<v-divider
v-else-if="item.divider"
:key="i"
dark
class="my-4"
/>
<v-list-item
v-else
:key="i"
@click="$router.push('/projects/' + $route.params.id + '/' + item.link)"
>
<v-list-item-action>
<v-icon>
{{ item.icon }}
</v-icon>
</v-list-item-action>
<v-list-item-content>
<v-list-item-title>
{{ item.text }}
</v-list-item-title>
</v-list-item-content>
</v-list-item>
</template>
</v-list>
</template>
<script>
export default {
data: () => ({
items: [
// { icon: 'lightbulb_outline', text: 'Start Labeling' },
{ divider: true },
{ icon: 'mdi-database', text: 'Dataset', link: 'dataset' },
{ icon: 'person', text: 'User', link: 'users' },
{ icon: 'label', text: 'Label', link: 'labels' },
{ divider: true },
// { heading: 'Labels' },
// { icon: 'add', text: 'Create new label', link: 'labels' },
// { divider: true },
{ icon: 'backup', text: 'Import', link: 'upload' },
{ icon: 'archive', text: 'Export', link: 'download' },
{ divider: true },
{ icon: 'settings', text: 'Guideline', link: 'guideline' },
{ icon: 'chat_bubble', text: 'Statistics', link: 'statistics' },
{ icon: 'help', text: 'Help', link: 'help' }
// { icon: 'keyboard', text: 'Keyboard shortcuts' }
]
})
}
</script>

208
frontend/components/project/SideBarRight/SideBarLabeling.vue

@ -0,0 +1,208 @@
<template>
<v-navigation-drawer
:value="drawer"
app
clipped
right
>
<v-list dense>
<v-list-group
sub-group
value="true"
>
<template v-slot:activator>
<v-list-item>
<v-list-item-content>
<v-list-item-title>
Progress
</v-list-item-title>
</v-list-item-content>
</v-list-item>
</template>
<v-list-item class="pl-4">
<v-list-item-content>
<v-list-item-title>
<v-progress-linear
:value="progress"
height="25"
rounded
>
<strong>{{ Math.ceil(progress) }}%</strong>
</v-progress-linear>
</v-list-item-title>
</v-list-item-content>
</v-list-item>
</v-list-group>
<v-list-group
sub-group
value="true"
>
<template v-slot:activator>
<v-list-item>
<v-list-item-content>
<v-list-item-title>
Labels
</v-list-item-title>
</v-list-item-content>
</v-list-item>
</template>
<v-list-item
v-for="(label, i) in labels"
:key="i"
class="pl-4"
>
<v-list-item-content>
<v-list-item-title>
<v-chip
:color="label.color"
text-color="white"
>
<v-avatar left>
<span class="white--text">{{ label.shortcut }}</span>
</v-avatar>
{{ label.name }}
</v-chip>
</v-list-item-title>
</v-list-item-content>
</v-list-item>
</v-list-group>
<v-list-item @click="showMetadata = true">
<v-list-item-action>
<v-icon>mdi-file-document-box</v-icon>
</v-list-item-action>
<v-list-item-content>
<v-list-item-title>
Show metadata
</v-list-item-title>
</v-list-item-content>
</v-list-item>
<v-list-item @click="showGuideline = true">
<v-list-item-action>
<v-icon>mdi-text-subject</v-icon>
</v-list-item-action>
<v-list-item-content>
<v-list-item-title>
Show guideline
</v-list-item-title>
</v-list-item-content>
</v-list-item>
</v-list>
<v-dialog
v-model="showMetadata"
width="800"
>
<v-card>
<v-card-title
class="headline grey lighten-2"
primary-title
>
Metadata
</v-card-title>
<v-card-text>
<v-sheet>
<pre>{{ prettyJson }}</pre>
</v-sheet>
</v-card-text>
<v-divider />
<v-card-actions>
<v-spacer />
<v-btn
color="primary"
text
@click="showMetadata = false"
>
Close
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog
v-model="showGuideline"
width="800"
>
<v-card>
<v-card-title
class="headline grey lighten-2"
primary-title
>
Annotation Guideline
</v-card-title>
<v-card-text>
<viewer
:value="guidelineText"
/>
</v-card-text>
<v-divider />
<v-card-actions>
<v-spacer />
<v-btn
color="primary"
text
@click="showGuideline = false"
>
Close
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-navigation-drawer>
</template>
<script>
import 'tui-editor/dist/tui-editor-contents.css'
import 'highlight.js/styles/github.css'
import { Viewer } from '@toast-ui/vue-editor'
export default {
components: {
Viewer
},
props: {
labels: {
type: Array,
default: () => ([]),
required: true
},
progress: {
type: Number,
default: 0,
required: true
},
metadata: {
type: String,
default: '{}',
required: true
},
guidelineText: {
type: String,
default: '# This is Viewer.\n Hello World.'
}
},
data: () => ({
showMetadata: false,
showGuideline: false
}),
computed: {
drawer() {
return this.$store.state.sidebar.drawer
},
prettyJson() {
const data = JSON.parse(this.metadata)
const pretty = JSON.stringify(data, null, 4)
return pretty
}
}
}
</script>

17
frontend/jest.config.js

@ -0,0 +1,17 @@
module.exports = {
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/$1',
'^~/(.*)$': '<rootDir>/$1',
'^vue$': 'vue/dist/vue.common.js'
},
moduleFileExtensions: ['js', 'vue', 'json'],
transform: {
'^.+\\.js$': 'babel-jest',
'.*\\.(vue)$': 'vue-jest'
},
collectCoverage: true,
collectCoverageFrom: [
'<rootDir>/components/**/*.vue',
'<rootDir>/pages/**/*.vue'
]
}

38
frontend/layouts/BaseLayout.vue

@ -0,0 +1,38 @@
<template>
<v-app id="keep">
<v-app-bar
app
clipped-left
clipped-right
color=""
>
<slot name="leftDrawerIcon" />
<nuxt-link to="/" class="top">
<span class="title ml-3 mr-5">doccano</span>
</nuxt-link>
<v-text-field
solo-inverted
flat
hide-details
label="Search"
prepend-inner-icon="search"
/>
<v-spacer />
<slot name="rightDrawerIcon" />
</v-app-bar>
<slot name="leftDrawer" />
<slot name="rightDrawer" />
<slot name="content" />
</v-app>
</template>
<script>
</script>
<style scoped>
.top{
text-decoration: none;
}
</style>

7
frontend/layouts/README.md

@ -0,0 +1,7 @@
# LAYOUTS
**This directory is not required, you can delete it if you don't want to use it.**
This directory contains your Application Layouts.
More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/views#layouts).

61
frontend/layouts/annotation.vue

@ -0,0 +1,61 @@
<template>
<base-layout>
<template #leftDrawerIcon>
<v-app-bar-nav-icon @click="drawerLeft = !drawerLeft" />
</template>
<template #rightDrawerIcon>
<v-app-bar-nav-icon @click="toggle" />
</template>
<template #leftDrawer>
<v-navigation-drawer
v-model="drawerLeft"
app
clipped
color=""
>
<side-bar-left />
</v-navigation-drawer>
</template>
<!--
<template #rightDrawer>
<v-navigation-drawer
v-model="drawerRight"
app
clipped
right
>
<slot name="sideBarRightContent" />
</v-navigation-drawer>
</template>
-->
<template #content>
<nuxt />
</template>
</base-layout>
</template>
<script>
import { mapMutations } from 'vuex'
import BaseLayout from '~/layouts/BaseLayout'
import SideBarLeft from '~/components/project/SideBarLeft'
export default {
components: {
BaseLayout,
SideBarLeft
},
data: () => ({
drawerLeft: false,
drawerRight: false
}),
methods: {
...mapMutations({
toggle: 'sidebar/toggle'
})
}
}
</script>

50
frontend/layouts/default.vue

@ -0,0 +1,50 @@
<template>
<v-app>
<v-app-bar
color="indigo lighten-1"
dark
app
>
<v-toolbar-title v-text="title" />
</v-app-bar>
<nuxt />
<v-footer
color="primary lighten-1"
padless
>
<v-layout
justify-center
wrap
>
<v-flex
primary
lighten-2
py-4
text-center
white--text
xs12
>
{{ new Date().getFullYear() }} <strong>Vuetify</strong>
</v-flex>
</v-layout>
</v-footer>
</v-app>
</template>
<script>
export default {
data() {
return {
title: 'doccano',
links: [
'Home',
'About Us',
'Team',
'Services',
'Blog',
'Contact Us'
]
}
}
}
</script>

44
frontend/layouts/error.vue

@ -0,0 +1,44 @@
<template>
<v-app dark>
<h1 v-if="error.statusCode === 404">
{{ pageNotFound }}
</h1>
<h1 v-else>
{{ otherError }}
</h1>
<NuxtLink to="/">
Home page
</NuxtLink>
</v-app>
</template>
<script>
export default {
layout: 'empty',
props: {
error: {
type: Object,
default: null
}
},
head() {
const title =
this.error.statusCode === 404 ? this.pageNotFound : this.otherError
return {
title
}
},
data() {
return {
pageNotFound: '404 Not Found',
otherError: 'An error occurred'
}
}
}
</script>
<style scoped>
h1 {
font-size: 20px;
}
</style>

37
frontend/layouts/project.vue

@ -0,0 +1,37 @@
<template>
<base-layout>
<template #leftDrawerIcon>
<v-app-bar-nav-icon @click="drawerLeft = !drawerLeft" />
</template>
<template #leftDrawer>
<v-navigation-drawer
v-model="drawerLeft"
app
clipped
color=""
>
<side-bar-left />
</v-navigation-drawer>
</template>
<template #content>
<nuxt />
</template>
</base-layout>
</template>
<script>
import BaseLayout from '~/layouts/BaseLayout'
import SideBarLeft from '~/components/project/SideBarLeft'
export default {
components: {
BaseLayout,
SideBarLeft
},
data: () => ({
drawerLeft: false
})
}
</script>

10
frontend/layouts/projectList.vue

@ -0,0 +1,10 @@
<template>
<v-app>
<nuxt />
</v-app>
</template>
<script>
export default {
}
</script>

8
frontend/middleware/README.md

@ -0,0 +1,8 @@
# MIDDLEWARE
**This directory is not required, you can delete it if you don't want to use it.**
This directory contains your application middleware.
Middleware let you define custom functions that can be run before rendering either a page or a group of pages.
More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/routing#middleware).

90
frontend/nuxt.config.js

@ -0,0 +1,90 @@
import colors from 'vuetify/es5/util/colors'
export default {
mode: 'spa',
/*
** Headers of the page
*/
head: {
titleTemplate: '%s - ' + process.env.npm_package_name,
title: process.env.npm_package_name || '',
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ hid: 'description', name: 'description', content: process.env.npm_package_description || '' }
],
script: [
{ src: 'https://use.fontawesome.com/releases/v5.0.6/js/all.js' }
],
link: [
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' },
{
rel: 'stylesheet',
href:
'https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|Material+Icons'
}
]
},
/*
** Customize the progress-bar color
*/
loading: { color: '#fff' },
/*
** Global CSS
*/
css: [
],
/*
** Plugins to load before mounting the App
*/
plugins: [
'~/plugins/filters.js'
],
/*
** Nuxt.js modules
*/
modules: [
'@nuxtjs/vuetify',
// Doc: https://axios.nuxtjs.org/usage
'@nuxtjs/axios',
'@nuxtjs/eslint-module'
],
/*
** Axios module configuration
** See https://axios.nuxtjs.org/options
*/
axios: {
},
/*
** vuetify module configuration
** https://github.com/nuxt-community/vuetify-module
*/
vuetify: {
theme: {
primary: colors.blue.darken2,
accent: colors.grey.darken3,
secondary: colors.amber.darken3,
info: colors.teal.lighten1,
warning: colors.amber.base,
error: colors.deepOrange.accent4,
success: colors.green.accent3
}
},
/*
** Build configuration
*/
build: {
/*
** You can extend webpack config here
*/
extend(config, ctx) {
config.module.rules.push({
test: /\.(txt|csv|conll|jsonl)$/i,
loader: 'file-loader',
options: {
name: '[path][name].[ext]'
}
})
}
}
}

47
frontend/package.json

@ -0,0 +1,47 @@
{
"name": "doccano-client",
"version": "1.0.0",
"description": "doccano client",
"author": "Hironsan",
"private": true,
"scripts": {
"lint": "eslint --ext .js,.vue --ignore-path .gitignore .",
"precommit": "yarn lint",
"test": "jest",
"dev": "nuxt",
"build": "nuxt build",
"start": "nuxt start",
"generate": "nuxt generate"
},
"dependencies": {
"@nuxtjs/axios": "^5.3.6",
"@nuxtjs/vuetify": "^1.0.2",
"@toast-ui/vue-editor": "^1.1.1",
"codemirror": "^5.48.2",
"nuxt": "^2.0.0",
"tui-editor": "^1.4.5",
"vuetify": "^2.0.2"
},
"devDependencies": {
"@nuxtjs/eslint-config": "^0.0.1",
"@nuxtjs/eslint-module": "^0.0.1",
"@vue/test-utils": "^1.0.0-beta.27",
"babel-core": "7.0.0-bridge.0",
"babel-eslint": "^10.0.1",
"babel-jest": "^24.1.0",
"eslint": "^5.15.1",
"eslint-config-standard": ">=12.0.0",
"eslint-plugin-import": ">=2.16.0",
"eslint-plugin-jest": ">=22.3.0",
"eslint-plugin-node": ">=8.0.1",
"eslint-plugin-nuxt": ">=0.4.2",
"eslint-plugin-promise": ">=4.0.1",
"eslint-plugin-standard": ">=4.0.0",
"eslint-plugin-vue": "^5.2.2",
"jest": "^24.1.0",
"nodemon": "^1.18.9",
"stylus": "^0.54.5",
"stylus-loader": "^3.0.2",
"vue-jest": "^3.0.3"
}
}

6
frontend/pages/README.md

@ -0,0 +1,6 @@
# PAGES
This directory contains your Application Views and Routes.
The framework reads all the `*.vue` files inside this directory and creates the router of your application.
More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/routing).

116
frontend/pages/index.vue

@ -0,0 +1,116 @@
<template>
<v-content>
<section>
<v-parallax src="/images/hero.jpeg" height="600">
<v-layout
column
align-center
justify-center
class="white--text"
>
<img src="/images/vuetify.png" alt="Vuetify.js" height="200">
<h1 class="white--text mb-2 display-1 text-xs-center">
Text Annotation for Human
</h1>
<div class="subheading mb-3 text-xs-center">
Just create project, upload data and start annotation. You can build dataset in hours.
</div>
<v-btn
class="blue lighten-2 mt-5"
dark
large
href="/pre-made-themes"
>
Get Started
</v-btn>
</v-layout>
</v-parallax>
</section>
<section>
<v-layout
column
wrap
class="my-5"
align-center
>
<v-flex xs12 sm4 class="my-3">
<div class="text-xs-center">
<h2 class="headline">
The best features
</h2>
</div>
</v-flex>
<v-flex xs12>
<v-container grid-list-xl>
<v-layout row wrap align-center>
<v-flex xs12 md4>
<v-card class="elevation-0 transparent">
<v-card-text class="text-xs-center">
<v-icon x-large class="blue--text text--lighten-2">
color_lens
</v-icon>
</v-card-text>
<v-card-title primary-title class="layout justify-center">
<div class="headline text-xs-center">
Team Collaboration
</div>
</v-card-title>
<v-card-text class="subheading">
Cras facilisis mi vitae nunc lobortis pharetra. Nulla volutpat tincidunt ornare.
Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.
Nullam in aliquet odio. Aliquam eu est vitae tellus bibendum tincidunt. Suspendisse potenti.
</v-card-text>
</v-card>
</v-flex>
<v-flex xs12 md4>
<v-card class="elevation-0 transparent">
<v-card-text class="text-xs-center">
<v-icon x-large class="blue--text text--lighten-2">
flash_on
</v-icon>
</v-card-text>
<v-card-title primary-title class="layout justify-center">
<div class="headline">
Multi Language
</div>
</v-card-title>
<v-card-text class="subheading">
Cras facilisis mi vitae nunc lobortis pharetra. Nulla volutpat tincidunt ornare.
Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.
Nullam in aliquet odio. Aliquam eu est vitae tellus bibendum tincidunt. Suspendisse potenti.
</v-card-text>
</v-card>
</v-flex>
<v-flex xs12 md4>
<v-card class="elevation-0 transparent">
<v-card-text class="text-xs-center">
<v-icon x-large class="blue--text text--lighten-2">
build
</v-icon>
</v-card-text>
<v-card-title primary-title class="layout justify-center">
<div class="headline text-xs-center">
Completely Open Sourced
</div>
</v-card-title>
<v-card-text class="subheading">
Cras facilisis mi vitae nunc lobortis pharetra. Nulla volutpat tincidunt ornare.
Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.
Nullam in aliquet odio. Aliquam eu est vitae tellus bibendum tincidunt. Suspendisse potenti.
</v-card-text>
</v-card>
</v-flex>
</v-layout>
</v-container>
</v-flex>
</v-layout>
</section>
</v-content>
</template>
<script>
export default {
components: {
}
}
</script>

23
frontend/pages/inspire.vue

@ -0,0 +1,23 @@
<template>
<v-content>
<v-container>
<v-layout>
<v-flex text-xs-center>
<img
src="/v.png"
alt="Vuetify.js"
class="mb-5"
>
<blockquote class="blockquote">
&#8220;First, solve the problem. Then, write the code.&#8221;
<footer>
<small>
<em>&mdash;John Johnson</em>
</small>
</footer>
</blockquote>
</v-flex>
</v-layout>
</v-container>
</v-content>
</template>

205
frontend/pages/projects/_id/annotation/index.vue

@ -0,0 +1,205 @@
<template>
<div>
<side-bar-labeling
:labels="labels"
:progress="progress"
:metadata="metadata"
/>
<v-content>
<v-container fluid fill-height>
<v-layout justify-center>
<v-flex>
<v-card>
<v-card-text class="title">
<entity-item-container :content="text" :labels="labels" :entities="annotations" />
</v-card-text>
</v-card>
</v-flex>
</v-layout>
</v-container>
</v-content>
</div>
</template>
<script>
import EntityItemContainer from '~/components/EntityItemContainer'
import SideBarLabeling from '~/components/project/SideBarRight/SideBarLabeling'
export default {
layout: 'annotation',
components: {
EntityItemContainer,
SideBarLabeling
},
data: () => ({
progress: 30,
metadata: '{"wikiPageId":2}',
search: '',
content: 'Sony',
text:
'Barack Hussein Obama II (born August 4, 1961) is an American attorney and politician who served as the 44th president of the United States from 2009 to 2017. A member of the Democratic Party, he was the first African American to be elected to the presidency. He previously served as a U.S. senator from Illinois from 2005 to 2008 and an Illinois state senator from 1997 to 2004.',
labelName: 'ORG',
labels: [
{
id: 1,
name: 'Location',
color: '#E91E63',
shortcut: 'l'
},
{
id: 2,
name: 'Organization',
color: '#03A9F4',
shortcut: 'o'
},
{
id: 3,
name: 'Person',
color: '#009688',
shortcut: 'p'
},
{
id: 4,
name: 'Date',
color: '#FF6F00',
shortcut: 'd'
},
{
id: 5,
name: 'Other',
color: '#333333',
shortcut: 't'
}
],
annotations: [
{
id: 2,
prob: 0.0,
label: 3,
start_offset: 0,
end_offset: 23,
user: 1,
document: 1
},
{
id: 3,
prob: 0.0,
label: 4,
start_offset: 30,
end_offset: 44,
user: 1,
document: 1
},
{
id: 2,
prob: 0.0,
label: 1,
start_offset: 125,
end_offset: 138,
user: 1,
document: 1
},
{
id: 2,
prob: 0.0,
label: 4,
start_offset: 144,
end_offset: 148,
user: 1,
document: 1
},
{
id: 2,
prob: 0.0,
label: 4,
start_offset: 152,
end_offset: 156,
user: 1,
document: 1
},
{
id: 2,
prob: 0.0,
label: 2,
start_offset: 174,
end_offset: 190,
user: 1,
document: 1
},
{
id: 2,
prob: 0.0,
label: 1,
start_offset: 285,
end_offset: 289,
user: 1,
document: 1
},
{
id: 2,
prob: 0.0,
label: 1,
start_offset: 303,
end_offset: 311,
user: 1,
document: 1
},
{
id: 2,
prob: 0.0,
label: 4,
start_offset: 317,
end_offset: 321,
user: 1,
document: 1
},
{
id: 2,
prob: 0.0,
label: 4,
start_offset: 325,
end_offset: 329,
user: 1,
document: 1
},
{
id: 2,
prob: 0.0,
label: 1,
start_offset: 337,
end_offset: 345,
user: 1,
document: 1
},
{
id: 2,
prob: 0.0,
label: 4,
start_offset: 365,
end_offset: 369,
user: 1,
document: 1
},
{
id: 2,
prob: 0.0,
label: 4,
start_offset: 373,
end_offset: 377,
user: 1,
document: 1
}
]
}),
methods: {
save() {},
cancel() {},
open() {},
close() {}
}
}
</script>
<style scoped>
</style>

142
frontend/pages/projects/_id/dataset/index.vue

@ -0,0 +1,142 @@
<template>
<v-content>
<v-container
fluid
fill-height
>
<v-layout
justify-center
>
<v-flex>
<v-card>
<v-card-title>
<v-btn
class="mb-2 text-capitalize"
outlined
:disabled="selected.length === 0"
@click="openRemoveModal"
>
Remove
</v-btn>
<Modal
ref="removeDialogue"
:title="removeModal.title"
:button="removeModal.button"
>
Are you sure you want to remove these documents from this project?
<v-list dense>
<v-list-item v-for="(doc, i) in selected" :key="i">
<v-list-item-content>
<v-list-item-title>{{ doc.text | truncate(50) }}</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="docs"
item-key="id"
:search="search"
show-select
>
<template v-slot:top>
<v-text-field
v-model="search"
prepend-inner-icon="search"
label="Search"
single-line
hide-details
filled
/>
</template>
<template v-slot:item.text="{ item }">
<v-edit-dialog
:return-value.sync="item.text"
large
>
<span class="d-flex d-sm-none">{{ item.text | truncate(50) }}</span>
<span class="d-none d-sm-flex">{{ item.text | truncate(200) }}</span>
<!--{{ item.text | truncate(200) }}-->
<template v-slot:input>
<v-textarea
v-model="item.text"
label="Edit"
/>
</template>
</v-edit-dialog>
</template>
</v-data-table>
</v-card>
</v-flex>
</v-layout>
</v-container>
</v-content>
</template>
<script>
import Modal from '~/components/Modal'
export default {
layout: 'project',
components: {
Modal
},
data: () => ({
search: '',
selected: [],
removeModal: {
title: 'Remove Document',
button: 'Yes, remove'
},
headers: [
{
text: 'Text',
align: 'left',
value: 'text'
}
],
docs: [
{
id: 1,
text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'
},
{
id: 2,
text: 'Text 1'
},
{
id: 3,
text: 'Text 2'
},
{
id: 4,
text: 'Text 3'
},
{
id: 5,
text: 'Text 4'
}
]
}),
methods: {
save() {
// send server
},
cancel() {
},
open() {
},
close() {
},
openAddModal() {
this.$refs.childDialogue.open()
},
openRemoveModal() {
this.$refs.removeDialogue.open()
}
}
}
</script>

9
frontend/pages/projects/_id/download/index.vue

@ -0,0 +1,9 @@
<template>
<div />
</template>
<script>
export default {
layout: 'project'
}
</script>

50
frontend/pages/projects/_id/guideline/index.vue

@ -0,0 +1,50 @@
<template>
<v-content>
<v-container
fluid
fill-height
>
<v-layout
justify-center
>
<v-flex fill-height>
<editor
v-model="editorText"
:options="editorOptions"
:visible="editorVisible"
preview-style="vertical"
height="inherit"
/>
</v-flex>
</v-layout>
</v-container>
</v-content>
</template>
<script>
import 'tui-editor/dist/tui-editor.css'
import 'tui-editor/dist/tui-editor-contents.css'
import 'codemirror/lib/codemirror.css'
import { Editor } from '@toast-ui/vue-editor'
export default {
layout: 'project',
components: {
Editor
},
data() {
return {
editorText: 'Write annotation guideline.',
editorOptions: {
},
editorVisible: true
}
}
}
</script>
<style>
.te-md-container .CodeMirror, .tui-editor-contents {
font-size: 20px;
}
</style>

116
frontend/pages/projects/_id/index.vue

@ -0,0 +1,116 @@
<template>
<v-content>
<section>
<v-parallax src="/images/hero.jpeg" height="600">
<v-layout
column
align-center
justify-center
class="white--text"
>
<img src="/images/vuetify.png" alt="Vuetify.js" height="200">
<h1 class="white--text mb-2 display-1 text-xs-center">
Text Annotation for Human
</h1>
<div class="subheading mb-3 text-xs-center">
Just create project, upload data and start annotation. You can build dataset in hours.
</div>
<v-btn
class="blue lighten-2 mt-5"
dark
large
href="/pre-made-themes"
>
Get Started
</v-btn>
</v-layout>
</v-parallax>
</section>
<section>
<v-layout
column
wrap
class="my-5"
align-center
>
<v-flex xs12 sm4 class="my-3">
<div class="text-xs-center">
<h2 class="headline">
The best features
</h2>
</div>
</v-flex>
<v-flex xs12>
<v-container grid-list-xl>
<v-layout row wrap align-center>
<v-flex xs12 md4>
<v-card class="elevation-0 transparent">
<v-card-text class="text-xs-center">
<v-icon x-large class="blue--text text--lighten-2">
color_lens
</v-icon>
</v-card-text>
<v-card-title primary-title class="layout justify-center">
<div class="headline text-xs-center">
Team Collaboration
</div>
</v-card-title>
<v-card-text class="subheading">
Cras facilisis mi vitae nunc lobortis pharetra. Nulla volutpat tincidunt ornare.
Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.
Nullam in aliquet odio. Aliquam eu est vitae tellus bibendum tincidunt. Suspendisse potenti.
</v-card-text>
</v-card>
</v-flex>
<v-flex xs12 md4>
<v-card class="elevation-0 transparent">
<v-card-text class="text-xs-center">
<v-icon x-large class="blue--text text--lighten-2">
flash_on
</v-icon>
</v-card-text>
<v-card-title primary-title class="layout justify-center">
<div class="headline">
Multi Language
</div>
</v-card-title>
<v-card-text class="subheading">
Cras facilisis mi vitae nunc lobortis pharetra. Nulla volutpat tincidunt ornare.
Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.
Nullam in aliquet odio. Aliquam eu est vitae tellus bibendum tincidunt. Suspendisse potenti.
</v-card-text>
</v-card>
</v-flex>
<v-flex xs12 md4>
<v-card class="elevation-0 transparent">
<v-card-text class="text-xs-center">
<v-icon x-large class="blue--text text--lighten-2">
build
</v-icon>
</v-card-text>
<v-card-title primary-title class="layout justify-center">
<div class="headline text-xs-center">
Completely Open Sourced
</div>
</v-card-title>
<v-card-text class="subheading">
Cras facilisis mi vitae nunc lobortis pharetra. Nulla volutpat tincidunt ornare.
Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.
Nullam in aliquet odio. Aliquam eu est vitae tellus bibendum tincidunt. Suspendisse potenti.
</v-card-text>
</v-card>
</v-flex>
</v-layout>
</v-container>
</v-flex>
</v-layout>
</section>
</v-content>
</template>
<script>
export default {
components: {
}
}
</script>

253
frontend/pages/projects/_id/labels/index.vue

@ -0,0 +1,253 @@
<template>
<v-content>
<v-container
fluid
fill-height
>
<v-layout
justify-center
>
<v-flex>
<v-card>
<v-card-title>
<v-btn
class="mb-2 text-capitalize"
color="primary"
@click="openAddModal"
>
Add Label
</v-btn>
<Modal
ref="childDialogue"
:title="addModal.title"
:button="addModal.button"
>
<v-text-field
label="Label name"
prepend-icon="label"
/>
<v-select
:items="keys"
label="Key"
prepend-icon="mdi-keyboard"
/>
<v-color-picker
v-model="color"
show-swatches
hide-mode-switch
width="800"
:mode.sync="mode"
class="ma-2"
/>
</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"
>
Are you sure you want to remove these labels from this project?
<v-list dense>
<v-list-item v-for="(label, i) in selected" :key="i">
<v-list-item-content>
<v-list-item-title>{{ label.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="labels"
item-key="id"
:search="search"
show-select
>
<template v-slot:top>
<v-text-field
v-model="search"
prepend-inner-icon="search"
label="Search"
single-line
hide-details
filled
/>
</template>
<template v-slot:item.name="{ item }">
<v-edit-dialog
:return-value.sync="item.name"
>
{{ item.name }}
<template v-slot:input>
<v-text-field
v-model="item.name"
label="Edit"
single-line
/>
</template>
</v-edit-dialog>
</template>
<template v-slot:item.shortcut="{ item }">
<v-edit-dialog
:return-value.sync="item.shortcut"
large
persistent
@save="save"
>
<div>{{ item.shortcut }}</div>
<template v-slot:input>
<div class="mt-4 title">
Update key
</div>
</template>
<template v-slot:input>
<v-select
v-model="item.shortcut"
:items="keys"
label="Key"
/>
</template>
</v-edit-dialog>
</template>
<template v-slot:item.color="{ item }">
<v-edit-dialog
:return-value.sync="item.color"
large
persistent
@save="save"
>
<v-chip :color="item.color" dark>
{{ item.color }}
</v-chip>
<template v-slot:input>
<div class="mt-4 title">
Update color
</div>
</template>
<template v-slot:input>
<v-color-picker
v-model="item.color"
show-swatches
hide-mode-switch
width="800"
:mode.sync="mode"
class="ma-2"
/>
</template>
</v-edit-dialog>
</template>
</v-data-table>
</v-card>
</v-flex>
</v-layout>
</v-container>
</v-content>
</template>
<script>
import Modal from '~/components/Modal'
export default {
layout: 'project',
components: {
Modal
},
data: () => ({
dialog: false,
search: '',
isLoading: false,
selected: [],
selectedUser: null,
keys: 'abcdefghijklmnopqrstuvwxyz'.split(''),
mode: 'hexa',
color: '#FF00FF',
addModal: {
title: 'Add Label',
button: 'Add Label'
},
removeModal: {
title: 'Remove Label',
button: 'Yes, remove'
},
headers: [
{
text: 'Name',
align: 'left',
value: 'name'
},
{
text: 'Shortkey',
value: 'shortcut'
},
{
text: 'Color',
sortable: false,
value: 'color'
}
],
labels: [
{
id: 1,
name: 'Location',
color: '#E91E63',
shortcut: 'l',
fat: 6.0
},
{
id: 2,
name: 'Organization',
color: '#03A9F4',
shortcut: 'o',
fat: 9.0
},
{
id: 3,
name: 'Person',
color: '#009688',
shortcut: 'p',
fat: 16.0
},
{
id: 4,
name: 'Money',
color: '#FF6F00',
shortcut: 'm',
fat: 3.7
},
{
id: 5,
name: 'Other',
color: '#333333',
shortcut: 't',
fat: 16.0
}
]
}),
methods: {
save() {
// send server
},
cancel() {
},
open() {
},
close() {
},
openAddModal() {
this.$refs.childDialogue.open()
},
openRemoveModal() {
this.$refs.removeDialogue.open()
}
}
}
</script>

9
frontend/pages/projects/_id/statistics/index.vue

@ -0,0 +1,9 @@
<template>
<div />
</template>
<script>
export default {
layout: 'project'
}
</script>

120
frontend/pages/projects/_id/upload/index.vue

@ -0,0 +1,120 @@
<template>
<v-content>
<v-container
fluid
fill-height
>
<v-layout>
<v-flex>
<v-form
ref="form"
lazy-validation
>
<v-card>
<v-card flat>
<v-card-title>
Objective
</v-card-title>
<v-card-text>
<v-radio-group v-model="selectedTask">
<v-radio
v-for="(task, i) in tasks"
:key="i"
:label="task"
:value="task"
/>
</v-radio-group>
</v-card-text>
</v-card>
<v-card flat max-width="800">
<v-card-title>
Import text items
</v-card-title>
<v-card-text>
<v-radio-group v-model="selectedFormat">
<v-radio
v-for="(format, i) in formats"
:key="i"
:label="format.text"
:value="format"
/>
</v-radio-group>
<v-sheet color="black white--text" class="pa-3">
{{ selectedFormat }}
</v-sheet>
</v-card-text>
</v-card>
<v-card flat max-width="500">
<v-card-title>
Select file
</v-card-title>
<v-card-text>
<v-file-input :accept="acceptType" label="File input" />
</v-card-text>
</v-card>
<v-card-actions>
<v-btn>
Import Dataset
</v-btn>
</v-card-actions>
</v-card>
</v-form>
</v-flex>
</v-layout>
</v-container>
</v-content>
</template>
<script>
export default {
layout: 'project',
data() {
return {
selectedTask: null,
selectedFormat: null,
tasks: [
'Text Classification',
'Sequence Labeling',
'Seq2seq'
],
formats: [
{
type: 'csv',
text: 'Upload a CSV file from your computer',
accept: '.csv'
},
{
type: 'plain',
text: 'Upload text items from your computer',
accept: '.txt'
},
{
type: 'json',
text: 'Upload a JSON file from your computer',
accept: '.json,.jsonl'
}
]
}
},
computed: {
acceptType() {
if (this.selectedFormat) {
return this.selectedFormat.accept
} else {
return '.txt,.csv,.json,.jsonl'
}
}
},
methods: {
}
}
</script>
<style scoped>
body pre {
color: white;
}
</style>

218
frontend/pages/projects/_id/users/index.vue

@ -0,0 +1,218 @@
<template>
<v-content>
<v-container
fluid
fill-height
>
<v-layout
justify-center
>
<v-flex>
<v-card>
<v-card-title>
<v-btn
class="mb-2 text-capitalize"
color="primary"
@click="openAddModal"
>
Add User
</v-btn>
<Modal
ref="childDialogue"
:title="addModal.title"
:button="addModal.button"
>
<v-autocomplete
v-model="selectedUser"
:items="items"
:loading="isLoading"
:search-input.sync="username"
color="white"
hide-no-data
hide-selected
item-text="username"
label="User Search APIs"
placeholder="Start typing to Search"
prepend-icon="mdi-account"
return-object
/>
<v-select
:items="roles"
label="Role"
prepend-icon="mdi-account-card-details-outline"
/>
</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"
>
Are you sure you want to remove these users from this project?
<v-list dense>
<v-list-item v-for="(user, i) in selected" :key="i">
<v-list-item-content>
<v-list-item-title>{{ user.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="users"
item-key="name"
:search="search"
show-select
>
<template v-slot:top>
<v-text-field
v-model="search"
prepend-inner-icon="search"
label="Search"
single-line
hide-details
filled
/>
</template>
<template v-slot:item.role="props">
<v-edit-dialog
:return-value.sync="props.item.role"
large
persistent
@save="save"
>
<div>{{ props.item.role }}</div>
<template v-slot:input>
<div class="mt-4 title">
Update Role
</div>
</template>
<template v-slot:input>
<v-select
v-model="props.item.role"
:items="roles"
label="Role"
/>
</template>
</v-edit-dialog>
</template>
</v-data-table>
</v-card>
</v-flex>
</v-layout>
</v-container>
</v-content>
</template>
<script>
import Modal from '~/components/Modal'
export default {
layout: 'project',
components: {
Modal
},
data: () => ({
search: '',
username: '',
isLoading: false,
selected: [],
selectedUser: null,
roles: ['Admin', 'Member'],
addModal: {
title: 'Add User',
button: 'Add User'
},
removeModal: {
title: 'Remove User',
button: 'Yes, remove'
},
headers: [
{
text: 'Name',
align: 'left',
sortable: false,
value: 'name'
},
{ text: 'Role', value: 'role' }
],
users: [
{
name: 'Hiroki Nakayama',
role: 'Admin'
},
{
name: 'Takahiro Kubo',
role: 'Member'
},
{
name: 'Junya Kamura',
role: 'Member'
},
{
name: 'Yasufumi Taniguchi',
role: 'Member'
},
{
name: 'Ryo Sho',
role: 'Member'
}
],
items: [
{
id: 1,
username: 'Donald Trump',
Description: 'Daily cat facts'
},
{
id: 2,
username: 'Barack Obama',
Description: 'Pictures of cats from Tumblr'
}
]
}),
watch: {
username(val) {
// Items have already been requested
if (this.isLoading) return
this.isLoading = true
// Lazily load input items
// GET /users endpoint
// fetch('https://api.publicapis.org/entries')
// .then(res => res.json())
// .then((res) => {
// this.items.push({ username: 'Bush', id: this.items.length + 1 })
// })
// .catch((err) => {
// alert(err)
// })
// .finally(() => (this.isLoading = false))
}
},
methods: {
save() {
// send server
},
openAddModal() {
this.$refs.childDialogue.open()
},
openRemoveModal() {
this.$refs.removeDialogue.open()
}
}
}
</script>

334
frontend/pages/projects/index.vue

@ -0,0 +1,334 @@
<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-card-title>
<v-data-table
:headers="headers"
:items="desserts"
:search="search"
>
<template v-slot:item.calories="{ item }">
<v-chip :color="getColor(item.calories)" dark>
{{ item.calories }}
</v-chip>
</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>
export default {
layout: 'projectList',
props: {
source: {
type: String,
default: 'cat'
}
},
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' }
],
search: '',
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%'
},
{
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%'
}
]
}),
methods: {
getColor(calories) {
if (calories > 400) return 'red'
else if (calories > 200) return 'orange'
else return 'green'
}
}
}
</script>

7
frontend/plugins/README.md

@ -0,0 +1,7 @@
# PLUGINS
**This directory is not required, you can delete it if you don't want to use it.**
This directory contains Javascript plugins that you want to run before mounting the root Vue.js application.
More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/plugins).

15
frontend/plugins/filters.js

@ -0,0 +1,15 @@
import Vue from 'vue'
export const truncate = function (text, length, clamp) {
text = text || ''
clamp = clamp || '...'
length = length || 30
if (text.length <= length) {
return text
}
return text.substring(0, length) + clamp
}
Vue.filter('truncate', truncate)

8
frontend/plugins/utils.js

@ -0,0 +1,8 @@
export const idealColor = function (hexString) {
// W3c offers a formula for calculating ideal color:
// https://www.w3.org/TR/AERT/#color-contrast
const r = parseInt(hexString.substr(1, 2), 16)
const g = parseInt(hexString.substr(3, 2), 16)
const b = parseInt(hexString.substr(5, 2), 16)
return ((((r * 299) + (g * 587) + (b * 114)) / 1000) < 128) ? '#ffffff' : '#000000'
}

11
frontend/static/README.md

@ -0,0 +1,11 @@
# STATIC
**This directory is not required, you can delete it if you don't want to use it.**
This directory contains your static files.
Each file inside this directory is mapped to `/`.
Thus you'd want to delete this README.md before deploying to production.
Example: `/static/robots.txt` is mapped as `/robots.txt`.
More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/assets#static).

BIN
frontend/static/favicon.ico

Before After

BIN
frontend/static/images/hero.jpeg

Before After
Width: 2200  |  Height: 1457  |  Size: 650 KiB

BIN
frontend/static/images/vuetify.png

Before After
Width: 197  |  Height: 225  |  Size: 2.4 KiB

BIN
frontend/static/v.png

Before After
Width: 120  |  Height: 120  |  Size: 5.5 KiB

10
frontend/store/README.md

@ -0,0 +1,10 @@
# STORE
**This directory is not required, you can delete it if you don't want to use it.**
This directory contains your Vuex Store files.
Vuex Store option is implemented in the Nuxt.js framework.
Creating a file in this directory automatically activates the option in the framework.
More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/vuex-store).

9
frontend/store/sidebar.js

@ -0,0 +1,9 @@
export const state = () => ({
drawer: false
})
export const mutations = {
toggle(state) {
state.drawer = !state.drawer
}
}

55
frontend/test/Modal.spec.js

@ -0,0 +1,55 @@
import { shallowMount } from '@vue/test-utils'
import Vue from 'vue'
import Vuetify from 'vuetify'
import Modal from '@/components/Modal.vue'
Vue.use(Vuetify)
describe('Modal', () => {
test('can receive props', () => {
const propsData = {
title: 'test title',
button: 'test text'
}
const wrapper = shallowMount(Modal, { propsData })
expect(wrapper.props()).toEqual(propsData)
})
test('can insert content into slot', () => {
const wrapper = shallowMount(Modal, {
slots: {
default: '<div data-test="slotContent">slot content</div>'
}
})
const slotContent = wrapper.find('[data-test="slotContent"]')
expect(slotContent.exists()).toBe(true)
expect(slotContent.text()).toBe('slot content')
})
test('is closed by default', () => {
const wrapper = shallowMount(Modal)
expect(wrapper.vm.dialog).toBe(false)
})
test('can open dialog', () => {
const wrapper = shallowMount(Modal)
wrapper.vm.open()
expect(wrapper.vm.dialog).toBe(true)
})
test('can close after agree', () => {
const wrapper = shallowMount(Modal)
wrapper.vm.open()
expect(wrapper.vm.dialog).toBe(true)
wrapper.vm.agree()
expect(wrapper.vm.dialog).toBe(false)
})
test('can close after cancel', () => {
const wrapper = shallowMount(Modal)
wrapper.vm.open()
expect(wrapper.vm.dialog).toBe(true)
wrapper.vm.cancel()
expect(wrapper.vm.dialog).toBe(false)
})
})

18
frontend/test/filters.spec.js

@ -0,0 +1,18 @@
import { truncate } from '@/plugins/filters.js'
describe('Truncate', () => {
test('dont do nothing', () => {
const string = 'aiueo'
expect(truncate(string)).toEqual(string)
})
test('cut the string and add clamp if string length is larger than specified length', () => {
const string = 'aiueo'
expect(truncate(string, 3)).toEqual('aiu...')
})
test('dont cut anything if string length is smaller than specified length', () => {
const string = 'aiueo'
expect(truncate(string, 10)).toEqual(string)
})
})

10182
frontend/yarn.lock
File diff suppressed because it is too large
View File

Loading…
Cancel
Save