Browse Source

frontend comment feature

pull/1195/head
Paul 4 years ago
parent
commit
5742bac6ca
27 changed files with 629 additions and 103 deletions
  1. 1
      Pipfile
  2. 8
      README.md
  3. 28
      app/server/static/components/annotation.pug
  4. 66
      app/server/static/components/annotationMixin.js
  5. 4
      docs/project_structure.md
  6. 99
      frontend/components/containers/comments/Comment.vue
  7. 114
      frontend/components/containers/comments/CommentSection.vue
  8. 9
      frontend/components/containers/documents/DocumentList.vue
  9. 13
      frontend/components/organisms/annotation/EntityItemBox.vue
  10. 39
      frontend/components/organisms/documents/DocumentUploadForm.vue
  11. 4
      frontend/components/organisms/layout/TheSideBar.vue
  12. 6
      frontend/i18n/en/index.js
  13. 10
      frontend/i18n/en/projects/comments.js
  14. 2
      frontend/i18n/en/projects/dataset.js
  15. 2
      frontend/i18n/en/projects/errors.js
  16. 3
      frontend/i18n/en/projects/settings.js
  17. 2
      frontend/i18n/en/rules.js
  18. 8
      frontend/i18n/index.js
  19. 13
      frontend/layouts/annotation.vue
  20. 18
      frontend/package-lock.json
  21. 2
      frontend/package.json
  22. 26
      frontend/pages/projects/_id/comments/index.vue
  23. 225
      frontend/pages/projects/_id/settings/index.vue
  24. 2
      frontend/rules/index.js
  25. 10
      frontend/yarn.lock
  26. 2
      nginx/nginx.conf
  27. 16
      package-lock.json

1
Pipfile

@ -52,3 +52,4 @@ isort = "isort . -c"
wait_for_db = "python manage.py wait_for_db"
test = "python manage.py test api.tests"
migrate = "python manage.py migrate"
collectstatic = "python manage.py collectstatic --noinput"

8
README.md

@ -135,6 +135,14 @@ $ docker-compose -f docker-compose.dev.yml up
Go to <http://127.0.0.1:3000/>.
Run Backend API-Tests:
You can run the API-Tests for the backend with the following command:
```bash
docker exec doccano_backend_1 pipenv run app/manage.py test api
```
### Add annotators (optionally)
If you want to add annotators/annotation approvers, see [Frequently Asked Questions](./docs/faq.md)

28
app/server/static/components/annotation.pug

@ -106,28 +106,11 @@ div.columns(v-cloak="")
)
section.modal-card-body.modal-card-body-footer
vue-json-pretty(
v-bind:data="documentMetadata"
v-bind:data="displayDocumentMetadata"
v-bind:show-double-quotes="false"
v-bind:show-line="false"
)
div.modal(v-bind:class="{ 'is-active': isCommentActive }")
div.modal-background
div.modal-card
header.modal-card-head
p.modal-card-title Document Comment
button.delete(
v-on:click="toggleCommentModal()"
aria-label="close"
)
section.modal-card-body.modal-card-body-footer
textarea.textarea(
v-model="comment"
v-debounce="syncComment"
type="text"
placeholder="Add document comment here..."
)
div.columns.is-multiline.is-gapless.is-mobile.is-vertical-center
div.column.is-3
progress.progress.is-inline-block(
@ -135,7 +118,7 @@ div.columns(v-cloak="")
v-bind:value="achievement"
max="100"
) 30%
div.column.is-5
div.column.is-6
span.ml10
strong {{ total - remaining }}
| /
@ -161,13 +144,6 @@ div.columns(v-cloak="")
)
span.icon
i.fas.fa-box
div.column.is-1.has-text-right
a.button.tooltip.is-tooltip-bottom(
v-on:click="toggleCommentModal()"
v-bind:data-tooltip="'Click to comment on document.'"
)
span.icon
i.fas.fa-comment
div.columns
div.column

66
app/server/static/components/annotationMixin.js

@ -88,53 +88,18 @@ export default {
offset: getOffsetFromUrl(window.location.href),
picked: 'all',
ordering: '',
comments: [],
comment: '',
count: 0,
prevLimit: 0,
paginationPages: 0,
paginationPage: 0,
singleClassClassification: false,
isAnnotationApprover: false,
isCommentActive: false,
isMetadataActive: false,
isAnnotationGuidelineActive: false,
};
},
methods: {
async syncComment(text) {
const docId = this.docs[this.pageNumber].id;
const commentId = this.comments[this.pageNumber].id;
const hasText = text.trim().length > 0;
if (commentId && !hasText) {
await HTTP.delete(`docs/${docId}/comments/${commentId}`);
const comments = this.comments.slice();
comments[this.pageNumber] = { text: '', id: null };
this.comments = comments;
} else if (commentId && hasText) {
await HTTP.patch(`docs/${docId}/comments/${commentId}`, { text });
const comments = this.comments.slice();
comments[this.pageNumber].text = text;
this.comments = comments;
} else {
const response = await HTTP.post(`docs/${docId}/comments`, { text });
const comments = this.comments.slice();
comments[this.pageNumber] = response.data;
this.comments = comments;
}
},
async toggleCommentModal() {
if (this.isCommentActive) {
this.isCommentActive = false;
return;
}
this.comment = this.comments[this.pageNumber].text;
this.isCommentActive = true;
},
resetScrollbar() {
const textbox = this.$refs.textbox;
@ -278,20 +243,9 @@ export default {
});
},
async fetchComments() {
const responses = await Promise.all(this.docs.map(doc => HTTP.get(`docs/${doc.id}/comments`)));
this.comments = responses.map(response => response.data.results[0] || { text: '', id: null });
},
},
watch: {
pageNumber() {
this.comment = this.comments[this.pageNumber].text;
},
docs() {
this.fetchComments();
},
picked() {
this.submit();
@ -336,7 +290,13 @@ export default {
},
compiledMarkdown() {
return marked(this.guideline, {
const documentMetadata = this.documentMetadata;
const guideline = documentMetadata && documentMetadata.guideline
? documentMetadata.guideline
: this.guideline;
return marked(guideline, {
sanitize: true,
});
},
@ -354,6 +314,18 @@ export default {
: 'Click to approve annotations';
},
displayDocumentMetadata() {
let documentMetadata = this.documentMetadata;
if (documentMetadata == null) {
return null;
}
documentMetadata = { ...documentMetadata };
delete documentMetadata.guideline;
delete documentMetadata.documentSourceUrl;
return documentMetadata;
},
documentMetadata() {
return this.documentMetadataFor(this.pageNumber);
},

4
docs/project_structure.md

@ -16,11 +16,11 @@ Consider them:
**[app/](https://github.com/doccano/doccano/tree/master/app)**
The `app/api` directory contains backend code. See [below](#Backend).
The `app/` directory contains backend code. See [below](#Backend).
**[frontend/](https://github.com/doccano/doccano/tree/master/frontend)**
The `app/api` directory contains frontend code. See [below](#Frontend).
The `frontend/` directory contains frontend code. See [below](#Frontend).
**[docker-compose.dev.yml](https://github.com/doccano/doccano/blob/master/docker-compose.dev.yml)**

99
frontend/components/containers/comments/Comment.vue

@ -0,0 +1,99 @@
<template>
<div>
<v-timeline-item
small
>
<div class="font-weight-normal">
<strong>{{ comment.username }}</strong> @{{ comment.created_at | dateParse('YYYY-MM-DDTHH:mm:ss') | dateFormat('YYYY-MM-DD HH:mm') }}
<v-tooltip top>
<template v-slot:activator="{ on, attrs }">
<v-btn
v-if="comment.user == userId"
icon
color="green"
v-bind="attrs"
v-on="on"
@click="showEdit=true"
>
<v-icon>mdi-comment-edit-outline</v-icon>
</v-btn>
</template>
<span>Edit Comment</span>
</v-tooltip>
<v-tooltip top>
<template v-slot:activator="{ on, attrs }">
<v-btn
v-if="comment.user == userId"
icon
color="red"
v-bind="attrs"
v-on="on"
@click="$emit('delete-comment', comment)"
>
<v-icon>mdi-delete-outline</v-icon>
</v-btn>
</template>
<span>Delete Comment</span>
</v-tooltip>
</div>
<div v-if="!showEdit">
{{ comment.text }}
</div>
<div v-else>
<v-textarea
v-model="editText"
solo
/>
<div>
<v-btn
color="red"
@click="showEdit=false"
>
Close
</v-btn>
<v-btn
color="green"
@click="updateComment(editText)"
>
Update
</v-btn>
</div>
</div>
</v-timeline-item>
</div>
</template>
<script>
import Vue from 'vue'
import VueFilterDateFormat from '@vuejs-community/vue-filter-date-format'
import VueFilterDateParse from '@vuejs-community/vue-filter-date-parse'
Vue.use(VueFilterDateFormat)
Vue.use(VueFilterDateParse)
export default {
name: 'Comment',
props: {
comment: {
required: true,
type: Object
},
userId: {
required: true,
type: Number
}
},
data() {
return {
showEdit: false,
editText: this.comment.text
}
},
methods: {
updateComment(newText) {
this.showEdit = false
this.$emit('update-comment', this.comment.id, newText)
}
}
}
</script>

114
frontend/components/containers/comments/CommentSection.vue

@ -0,0 +1,114 @@
<template>
<div>
<div class="font-weight-bold ml-8 mb-2">
{{ this.$t('comments.comments') }}
</div>
<v-timeline
align-top
dense
>
<v-timeline-item
fill-dot
class="mb-12"
color="green"
large
>
<v-textarea
v-model="message"
outlined
name="CommentInput"
:label="this.$t('comments.message')"
value=""
/>
<v-btn
class="white--text"
color="green"
depressed
:disabled="message.length === 0"
@click="add"
>
{{ this.$t('comments.send') }}
</v-btn>
</v-timeline-item>
<comment
v-for="comment in comments"
:key="comment.id"
:comment="comment"
:user-id="userId"
@delete-comment="remove"
@update-comment="update"
/>
</v-timeline>
</div>
</template>
<script>
import { mapActions, mapState, mapMutations } from 'vuex'
import Comment from './Comment'
export default {
name: 'CommentSection',
components: { Comment },
fetch() {
this.getMyUserId()
},
data() {
return {
message: ''
}
},
computed: {
...mapState('documents', ['items', 'total', 'current', 'selected']),
...mapState('comments', ['comments', 'userId'])
},
watch: {
total() {
this.getCommentList({
projectId: this.$route.params.id,
docId: this.items[this.current].id
})
},
current: {
handler() {
if (this.total !== 0) {
this.getCommentList({
projectId: this.$route.params.id,
docId: this.items[this.current].id
})
}
},
immediate: true
}
},
methods: {
add() {
this.addComment({
text: this.message,
projectId: this.$route.params.id,
docId: this.items[this.current].id
})
this.message = ''
},
remove(comment) {
this.updateSelectedComments([comment])
this.deleteComment({
projectId: this.$route.params.id,
docId: this.items[this.current].id
})
},
update(commentId, text) {
this.updateComment({
projectId: this.$route.params.id,
docId: this.items[this.current].id,
commentId,
text
})
},
...mapActions('comments', ['addComment', 'getCommentList', 'deleteComment', 'updateComment', 'getMyUserId']),
...mapMutations('comments', ['updateSelectedComments'])
}
}
</script>

9
frontend/components/containers/documents/DocumentList.vue

@ -42,6 +42,9 @@
</template>
</v-edit-dialog>
</template>
<template v-slot:item.comment_count="{ item }">
<span> {{ item.comment_count }} </span>
</template>
<template v-slot:item.action="{ item }">
<v-btn
small
@ -82,6 +85,12 @@ export default {
value: 'meta',
sortable: false
},
{
text: this.$t('comments.comments'),
align: 'left',
value: 'comment_count',
sortable: true
},
{
text: this.$t('dataset.action'),
align: 'left',

13
frontend/components/organisms/annotation/EntityItemBox.vue

@ -98,24 +98,27 @@ export default {
chunks() {
let chunks = []
const entities = this.sortedEntities
let startOffset = 0
for (const entity of entities) {
// to count the number of characters correctly.
const characters = [...this.text]
for (const entity of this.sortedEntities) {
// add non-entities to chunks.
chunks = chunks.concat(this.makeChunks(this.text.slice(startOffset, entity.start_offset)))
let piece = characters.slice(startOffset, entity.start_offset).join('')
chunks = chunks.concat(this.makeChunks(piece))
startOffset = entity.end_offset
// add entities to chunks.
const label = this.labelObject[entity.label]
piece = characters.slice(entity.start_offset, entity.end_offset).join('')
chunks.push({
id: entity.id,
label: label.text,
color: label.background_color,
text: this.text.slice(entity.start_offset, entity.end_offset)
text: piece
})
}
// add the rest of text.
chunks = chunks.concat(this.makeChunks(this.text.slice(startOffset, this.text.length)))
chunks = chunks.concat(this.makeChunks(characters.slice(startOffset, characters.length).join('')))
return chunks
},

39
frontend/components/organisms/documents/DocumentUploadForm.vue

@ -18,7 +18,7 @@
type="error"
dismissible
>
{{ $t('errors.fileCannotUpload') }}
{{ $t('errors.fileCannotUpload') + errorMsg }}
</v-alert>
<h2>{{ $t('dataset.importDataMessage1') }}</h2>
<v-radio-group
@ -45,6 +45,7 @@
<h2>{{ $t('dataset.importDataMessage2') }}</h2>
<v-file-input
v-model="file"
multiple
:accept="acceptType"
:rules="uploadFileRules($t('rules.uploadFileRules'))"
:label="$t('labels.filePlaceholder')"
@ -81,7 +82,9 @@ export default {
selectedFormat: null,
fileFormatRules,
uploadFileRules,
showError: false
showError: false,
errors: [],
errorMsg: ''
}
},
@ -107,18 +110,32 @@ export default {
},
create() {
if (this.validate()) {
this.uploadDocument({
projectId: this.$route.params.id,
format: this.selectedFormat.type,
file: this.file
})
.then((response) => {
this.reset()
this.cancel()
this.errors = []
const promises = []
const id = this.$route.params.id
const type = this.selectedFormat.type
this.file.forEach((item) => {
promises.push({
projectId: id,
format: type,
file: item
})
.catch(() => {
})
let p = Promise.resolve()
promises.forEach((item) => {
p = p.then(() => this.uploadDocument(item)).catch(() => {
this.errors.push(item.file.name)
this.showError = true
})
})
p.finally(() => {
if (!this.errors.length) {
this.reset()
this.cancel()
} else {
this.errorMsg = this.errors.join(', ')
}
})
}
}
}

4
frontend/components/organisms/layout/TheSideBar.vue

@ -65,8 +65,10 @@ export default {
{ icon: 'mdi-database', text: this.$t('dataset.dataset'), link: 'dataset', adminOnly: true },
{ icon: 'label', text: this.$t('labels.labels'), link: 'labels', adminOnly: true },
{ icon: 'person', text: this.$t('members.members'), link: 'members', adminOnly: true },
{ icon: 'mdi-comment-account-outline', text: 'Comments', link: 'comments', adminOnly: true },
{ icon: 'mdi-book-open-outline', text: this.$t('guideline.guideline'), link: 'guideline', adminOnly: true },
{ icon: 'mdi-chart-bar', text: this.$t('statistics.statistics'), link: 'statistics', adminOnly: true }
{ icon: 'mdi-chart-bar', text: this.$t('statistics.statistics'), link: 'statistics', adminOnly: true },
{ icon: 'mdi-cog', text: this.$t('settings.title'), link: 'settings', adminOnly: true }
]
return items.filter(item => this.isVisible(item))
}

6
frontend/i18n/en/index.js

@ -12,8 +12,10 @@ import guideline from './projects/guideline'
import projectHome from './projects/home'
import labels from './projects/labels'
import members from './projects/members'
import comments from './projects/comments'
import overview from './projects/overview'
import statistics from './projects/statistics'
import settings from './projects/settings'
export default {
home,
@ -30,6 +32,8 @@ export default {
projectHome,
labels,
members,
comments,
overview,
statistics
statistics,
settings
}

10
frontend/i18n/en/projects/comments.js

@ -0,0 +1,10 @@
export default {
comments: 'Comments',
removeComment: 'Remove Comment',
removePrompt: 'Are you sure you want to remove these comments?',
commentView: 'View',
created_at: 'Created at',
document: 'Document',
send: 'Send',
message: 'Message'
}

2
frontend/i18n/en/projects/dataset.js

@ -9,7 +9,7 @@ export default {
annotate: 'Annotate',
importDataTitle: 'Upload Data',
importDataMessage1: 'Select a file format',
importDataMessage2: 'Select a file',
importDataMessage2: 'Select file(s)',
importDataPlaceholder: 'File input',
exportDataTitle: 'Export Data',
exportDataMessage: 'Select a file format',

2
frontend/i18n/en/projects/errors.js

@ -1,5 +1,5 @@
export default {
fileCannotUpload: 'The file could not be uploaded. Maybe invalid format.\n Please check available formats carefully.',
fileCannotUpload: 'The file(s) could not be uploaded. Maybe invalid format.\n Please check available formats and the following file(s): ',
labelCannotCreate: 'The label could not be created.\n You cannot use the same label name or shortcut key.',
invalidUserOrPass: 'Incorrect username or password, or something went wrong.'
}

3
frontend/i18n/en/projects/settings.js

@ -0,0 +1,3 @@
export default {
title: 'Settings'
}

2
frontend/i18n/en/rules.js

@ -29,7 +29,7 @@ export default {
},
uploadFileRules: {
fileRequired: 'File is required',
fileLessThan1MB: 'File size should be less than 1 MB!'
fileLessThan1MB: 'File size should be less than 100 MB!'
},
passwordRules: {
passwordRequired: 'Password is required',

8
frontend/i18n/index.js

@ -9,7 +9,7 @@ export default {
{
name: '中文',
code: 'zh',
iso: 'zh-CA',
iso: 'zh-CN',
file: 'zh'
},
{
@ -17,6 +17,12 @@ export default {
code: 'fr',
iso: 'fr-CA',
file: 'fr'
},
{
name: 'Deutsch',
code: 'de',
iso: 'de-DE',
file: 'de'
}
],
lazy: true,

13
frontend/layouts/annotation.vue

@ -39,7 +39,6 @@
v-model="filterOption"
/>
<guideline-button />
<comment-button />
<clear-annotations-button />
</v-col>
<v-spacer />
@ -62,8 +61,14 @@
</v-col>
</v-row>
</v-container>
<v-container fluid>
<v-row>
<v-col cols="12" md="9">
<comment-section />
</v-col>
</v-row>
</v-container>
</v-main>
<bottom-navigator
v-model="page"
:length="total"
@ -80,7 +85,7 @@ import GuidelineButton from '@/components/containers/annotation/GuidelineButton'
import MetadataBox from '@/components/organisms/annotation/MetadataBox'
import FilterButton from '@/components/containers/annotation/FilterButton'
import ApproveButton from '@/components/containers/annotation/ApproveButton'
import CommentButton from '@/components/containers/annotation/CommentButton'
import CommentSection from '../components/containers/comments/CommentSection.vue'
import Pagination from '~/components/containers/annotation/Pagination'
import TheHeader from '~/components/organisms/layout/TheHeader'
import TheSideBar from '~/components/organisms/layout/TheSideBar'
@ -98,7 +103,7 @@ export default {
ApproveButton,
MetadataBox,
ClearAnnotationsButton,
CommentButton
CommentSection
},
fetch() {

18
frontend/package-lock.json

@ -0,0 +1,18 @@
{
"name": "doccano",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@vuejs-community/vue-filter-date-format": {
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@vuejs-community/vue-filter-date-format/-/vue-filter-date-format-1.6.3.tgz",
"integrity": "sha512-fUjAAI/1qzJPR6AYoK00TBA6/aRYKTXjLY/rSYsWV4G5nwywKfrpOYOJi5exWyzozohyV6nrzliDlWcl32+IPg=="
},
"@vuejs-community/vue-filter-date-parse": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/@vuejs-community/vue-filter-date-parse/-/vue-filter-date-parse-1.1.6.tgz",
"integrity": "sha512-OzeiL5wrq+5+sw9hJfcfaIxcG953lpe37Lf0JnRKzVvfBRTHX2t8SIVxtDmgLhSGoGx9B4bGbpv4NaIqeEofXA=="
}
}
}

2
frontend/package.json

@ -19,6 +19,8 @@
"@nuxtjs/proxy": "^2.0.1",
"@nuxtjs/vuetify": "^1.11.2",
"@toast-ui/vue-editor": "^1.1.1",
"@vuejs-community/vue-filter-date-format": "^1.6.3",
"@vuejs-community/vue-filter-date-parse": "^1.1.6",
"chart.js": "^2.9.3",
"codemirror": "^5.55.0",
"js-cookie": "^2.2.1",

26
frontend/pages/projects/_id/comments/index.vue

@ -0,0 +1,26 @@
<template>
<v-card>
<v-card-title>
<comment-deletion-button />
</v-card-title>
<comment-list />
</v-card>
</template>
<script>
import CommentList from '@/components/containers/comments/CommentList'
import CommentDeletionButton from '@/components/containers/comments/CommentDeletionButton'
export default {
layout: 'project',
components: {
CommentList,
CommentDeletionButton
},
validate({ params }) {
return /^\d+$/.test(params.id)
}
}
</script>

225
frontend/pages/projects/_id/settings/index.vue

@ -0,0 +1,225 @@
<template>
<v-card>
<v-card-title class="mb-2">
<h2>About project</h2>
</v-card-title>
<v-card-text v-if="isReady">
<v-form
ref="form"
v-model="valid"
>
<v-row>
<v-col
cols="12"
sm="6"
>
<h3>Name</h3>
<v-text-field
v-model="project.name"
label="Add project name"
:rules="projectNameRules($t('rules.projectNameRules'))"
:disabled="!edit.name"
single-line
/>
</v-col>
<v-col
cols="12"
sm="6"
>
<v-btn
v-if="!edit.name"
outlined
color="grey"
class="text-capitalize"
@click="editProject('name')"
>
Edit
</v-btn>
<v-btn
v-if="edit.name"
outlined
color="primary"
class="text-capitalize"
@click="doneEdit()"
>
Save
</v-btn>
<v-btn
v-if="edit.name"
outlined
color="grey"
class="text-capitalize"
@click="cancelEdit()"
>
Cancel
</v-btn>
</v-col>
</v-row>
<v-row>
<v-col
cols="12"
sm="6"
>
<h3>Description</h3>
<v-text-field
v-model="project.description"
label="Add description"
:rules="descriptionRules($t('rules.descriptionRules'))"
:disabled="!edit.desc"
single-line
/>
</v-col>
<v-col
cols="12"
sm="6"
>
<v-btn
v-if="!edit.desc"
outlined
color="grey"
class="text-capitalize"
@click="editProject('desc')"
>
Edit
</v-btn>
<v-btn
v-if="edit.desc"
outlined
color="primary"
class="text-capitalize"
@click="doneEdit()"
>
Save
</v-btn>
<v-btn
v-if="edit.desc"
outlined
color="grey"
class="text-capitalize"
@click="cancelEdit()"
>
Cancel
</v-btn>
</v-col>
</v-row>
<v-row>
<v-col
cols="12"
sm="6"
>
<h3>Shuffle</h3>
<v-checkbox
v-model="project.randomize_document_order"
:label="$t('overview.randomizeDocOrder')"
/>
</v-col>
</v-row>
<v-row>
<v-col
cols="12"
sm="6"
>
<h3>Collaboration</h3>
<v-checkbox
v-model="project.collaborative_annotation"
:label="$t('overview.shareAnnotations')"
/>
</v-col>
</v-row>
</v-form>
</v-card-text>
</v-card>
</template>
<script>
import ProjectService from '@/services/project.service'
import { projectNameRules, descriptionRules } from '@/rules/index'
export default {
layout: 'project',
data() {
return {
project: {},
beforeEditCache: {},
edit: {
name: false,
desc: false
},
projectNameRules,
descriptionRules,
valid: false
}
},
computed: {
isReady() {
return !!this.project
}
},
watch: {
'project.randomize_document_order'() {
this.doneEdit()
},
'project.collaborative_annotation'() {
this.doneEdit()
}
},
created() {
const projectId = this.$route.params.id
ProjectService.fetchProjectById(projectId)
.then((response) => {
this.project = response.data
})
.catch((error) => {
alert(error)
})
},
methods: {
initEdit() {
Object.keys(this.edit).forEach((v) => { this.edit[v] = false })
},
editProject(name) {
this.cancelEdit()
this.edit[name] = true
Object.assign(this.beforeEditCache, this.project)
},
cancelEdit() {
this.initEdit()
Object.assign(this.project, this.beforeEditCache)
},
doneEdit() {
if (!this.validate()) {
this.cancelEdit()
return
}
const projectId = this.$route.params.id
ProjectService.updateProject(projectId, this.project)
.then((response) => {
this.project = response.data
this.beforeEditCache = {}
})
.catch((error) => {
alert(error)
})
.finally(() => {
this.initEdit()
})
},
validate() {
return this.$refs.form.validate()
}
},
validate({ params }) {
return /^\d+$/.test(params.id)
}
}
</script>

2
frontend/rules/index.js

@ -57,7 +57,7 @@ export const fileFormatRules = (msg) => {
export const uploadFileRules = (msg) => {
return [
v => !!v || msg.fileRequired,
v => !v || v.size < 1000000 || msg.fileLessThan1MB
v => !v || v.some(file => file.size < 100000000) || msg.fileLessThan1MB
]
}

10
frontend/yarn.lock

@ -1946,6 +1946,16 @@
lodash "^4.17.15"
pretty "^2.0.0"
"@vuejs-community/vue-filter-date-format@^1.6.3":
version "1.6.3"
resolved "https://registry.yarnpkg.com/@vuejs-community/vue-filter-date-format/-/vue-filter-date-format-1.6.3.tgz#10a9b11425f4288f3ebc14c79c7de29322dafec7"
integrity sha512-fUjAAI/1qzJPR6AYoK00TBA6/aRYKTXjLY/rSYsWV4G5nwywKfrpOYOJi5exWyzozohyV6nrzliDlWcl32+IPg==
"@vuejs-community/vue-filter-date-parse@^1.1.6":
version "1.1.6"
resolved "https://registry.yarnpkg.com/@vuejs-community/vue-filter-date-parse/-/vue-filter-date-parse-1.1.6.tgz#8679a990af0f74b24c58d210ff2341e5b884a059"
integrity sha512-OzeiL5wrq+5+sw9hJfcfaIxcG953lpe37Lf0JnRKzVvfBRTHX2t8SIVxtDmgLhSGoGx9B4bGbpv4NaIqeEofXA==
"@webassemblyjs/ast@1.9.0":
version "1.9.0"
resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.9.0.tgz#bd850604b4042459a5a41cd7d338cbed695ed964"

2
nginx/nginx.conf

@ -1,7 +1,7 @@
server {
listen 80;
charset utf-8;
client_max_body_size 100M;
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";

16
package-lock.json

@ -0,0 +1,16 @@
{
"requires": true,
"lockfileVersion": 1,
"dependencies": {
"@vuejs-community/vue-filter-date-format": {
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@vuejs-community/vue-filter-date-format/-/vue-filter-date-format-1.6.3.tgz",
"integrity": "sha512-fUjAAI/1qzJPR6AYoK00TBA6/aRYKTXjLY/rSYsWV4G5nwywKfrpOYOJi5exWyzozohyV6nrzliDlWcl32+IPg=="
},
"@vuejs-community/vue-filter-date-parse": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/@vuejs-community/vue-filter-date-parse/-/vue-filter-date-parse-1.1.6.tgz",
"integrity": "sha512-OzeiL5wrq+5+sw9hJfcfaIxcG953lpe37Lf0JnRKzVvfBRTHX2t8SIVxtDmgLhSGoGx9B4bGbpv4NaIqeEofXA=="
}
}
}
Loading…
Cancel
Save