From e973412b5ea29685f4fa260d8eb44baae095fb81 Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Sat, 4 Jul 2020 16:37:19 +0200 Subject: rename timestamp to annotation --- .../__pycache__/crud_controller.cpython-37.pyc | Bin 3968 -> 4047 bytes .../cli/app/controllers/annotation_controller.py | 18 +++ .../cli/app/controllers/paragraph_controller.py | 18 +++ .../cli/app/controllers/timestamp_controller.py | 20 --- animism-align/cli/app/server/web.py | 4 + .../cli/app/sql/__pycache__/common.cpython-37.pyc | Bin 910 -> 0 bytes .../cli/app/sql/__pycache__/env.cpython-37.pyc | Bin 1701 -> 0 bytes animism-align/cli/app/sql/common.py | 2 +- animism-align/cli/app/sql/env.py | 2 +- animism-align/cli/app/sql/models/annotation.py | 39 ++++++ animism-align/cli/app/sql/models/timestamp.py | 39 ------ .../sql/versions/202006271347_add_paragraphs.py | 56 -------- .../sql/versions/202007041633_create_database.py | 56 ++++++++ animism-align/frontend/actions.js | 9 +- animism-align/frontend/api/index.js | 6 - animism-align/frontend/store.js | 5 +- animism-align/frontend/types.js | 4 +- .../frontend/views/align/align.actions.js | 11 +- .../frontend/views/align/align.reducer.js | 21 +++ .../components/annotations/annotation.form.js | 95 ++++++------- .../views/align/containers/timeline.container.js | 2 +- .../views/annotation/annotation.reducer.js | 21 +++ .../views/timestamp/components/timestamp.form.js | 153 --------------------- .../views/timestamp/containers/timestamp.edit.js | 53 ------- .../views/timestamp/containers/timestamp.index.js | 53 ------- .../views/timestamp/containers/timestamp.new.js | 44 ------ .../frontend/views/timestamp/timestamp.reducer.js | 21 --- 27 files changed, 247 insertions(+), 505 deletions(-) create mode 100644 animism-align/cli/app/controllers/annotation_controller.py create mode 100644 animism-align/cli/app/controllers/paragraph_controller.py delete mode 100644 animism-align/cli/app/controllers/timestamp_controller.py delete mode 100644 animism-align/cli/app/sql/__pycache__/common.cpython-37.pyc delete mode 100644 animism-align/cli/app/sql/__pycache__/env.cpython-37.pyc create mode 100644 animism-align/cli/app/sql/models/annotation.py delete mode 100644 animism-align/cli/app/sql/models/timestamp.py delete mode 100644 animism-align/cli/app/sql/versions/202006271347_add_paragraphs.py create mode 100644 animism-align/cli/app/sql/versions/202007041633_create_database.py create mode 100644 animism-align/frontend/views/annotation/annotation.reducer.js delete mode 100644 animism-align/frontend/views/timestamp/components/timestamp.form.js delete mode 100644 animism-align/frontend/views/timestamp/containers/timestamp.edit.js delete mode 100644 animism-align/frontend/views/timestamp/containers/timestamp.index.js delete mode 100644 animism-align/frontend/views/timestamp/containers/timestamp.new.js delete mode 100644 animism-align/frontend/views/timestamp/timestamp.reducer.js diff --git a/animism-align/cli/app/controllers/__pycache__/crud_controller.cpython-37.pyc b/animism-align/cli/app/controllers/__pycache__/crud_controller.cpython-37.pyc index 7f7d98d..e28baa6 100644 Binary files a/animism-align/cli/app/controllers/__pycache__/crud_controller.cpython-37.pyc and b/animism-align/cli/app/controllers/__pycache__/crud_controller.cpython-37.pyc differ diff --git a/animism-align/cli/app/controllers/annotation_controller.py b/animism-align/cli/app/controllers/annotation_controller.py new file mode 100644 index 0000000..8d91d1c --- /dev/null +++ b/animism-align/cli/app/controllers/annotation_controller.py @@ -0,0 +1,18 @@ +from flask import request, jsonify, redirect +from flask_classful import route +from werkzeug.datastructures import MultiDict + +from app.sql.common import db, Session +from app.sql.models.annotation import Annotation, AnnotationForm +from app.controllers.crud_controller import CrudView + +class AnnotationView(CrudView): + model = Annotation + form = AnnotationForm + default_sort = "start_ts" + + def on_create(self, session, form, item): + item.settings = form['settings'] + + def on_update(self, session, form, item): + item.settings = form['settings'] diff --git a/animism-align/cli/app/controllers/paragraph_controller.py b/animism-align/cli/app/controllers/paragraph_controller.py new file mode 100644 index 0000000..8056f51 --- /dev/null +++ b/animism-align/cli/app/controllers/paragraph_controller.py @@ -0,0 +1,18 @@ +from flask import request, jsonify, redirect +from flask_classful import route +from werkzeug.datastructures import MultiDict + +from app.sql.common import db, Session +from app.sql.models.paragraph import Paragraph, ParagraphForm +from app.controllers.crud_controller import CrudView + +class ParagraphView(CrudView): + model = Paragraph + form = ParagraphForm + default_sort = "start_ts" + + def on_create(self, session, form, item): + item.settings = form['settings'] + + def on_update(self, session, form, item): + item.settings = form['settings'] diff --git a/animism-align/cli/app/controllers/timestamp_controller.py b/animism-align/cli/app/controllers/timestamp_controller.py deleted file mode 100644 index d4cef82..0000000 --- a/animism-align/cli/app/controllers/timestamp_controller.py +++ /dev/null @@ -1,20 +0,0 @@ -from flask import request, jsonify, redirect -from flask_classful import route -from werkzeug.datastructures import MultiDict - -from app.sql.common import db, Session -from app.sql.models.graph import Timestamp, TimestampForm -from app.sql.models.page import Page -from app.sql.models.tile import Tile -from app.controllers.crud_controller import CrudView - -class TimestampView(CrudView): - model = Timestamp - form = TimestampForm - default_sort = "start_ts" - - def on_create(self, session, form, item): - item.settings = form['settings'] - - def on_update(self, session, form, item): - item.settings = form['settings'] diff --git a/animism-align/cli/app/server/web.py b/animism-align/cli/app/server/web.py index c3a812a..8c7bbc2 100644 --- a/animism-align/cli/app/server/web.py +++ b/animism-align/cli/app/server/web.py @@ -16,6 +16,8 @@ from flask import Flask, Blueprint, send_from_directory, request from app.sql.common import db, connection_url from app.settings import app_cfg +from app.controllers.annotation_controller import AnnotationView +from app.controllers.paragraph_controller import ParagraphView from app.controllers.upload_controller import UploadView def create_app(script_info=None): @@ -32,6 +34,8 @@ def create_app(script_info=None): db.init_app(app) + AnnotationView.register(app, route_prefix='/api/v1/') + ParagraphView.register(app, route_prefix='/api/v1/') UploadView.register(app, route_prefix='/api/v1/') index_html = 'index.html' diff --git a/animism-align/cli/app/sql/__pycache__/common.cpython-37.pyc b/animism-align/cli/app/sql/__pycache__/common.cpython-37.pyc deleted file mode 100644 index f995f96..0000000 Binary files a/animism-align/cli/app/sql/__pycache__/common.cpython-37.pyc and /dev/null differ diff --git a/animism-align/cli/app/sql/__pycache__/env.cpython-37.pyc b/animism-align/cli/app/sql/__pycache__/env.cpython-37.pyc deleted file mode 100644 index 96f3d85..0000000 Binary files a/animism-align/cli/app/sql/__pycache__/env.cpython-37.pyc and /dev/null differ diff --git a/animism-align/cli/app/sql/common.py b/animism-align/cli/app/sql/common.py index d79bc06..f291daa 100644 --- a/animism-align/cli/app/sql/common.py +++ b/animism-align/cli/app/sql/common.py @@ -31,6 +31,6 @@ Base.metadata.bind = engine db = SQLAlchemy() # include the models in reverse dependency order, so relationships work -from app.sql.models.timestamp import Timestamp +from app.sql.models.annotation import Annotation from app.sql.models.paragraph import Paragraph from app.sql.models.upload import Upload diff --git a/animism-align/cli/app/sql/env.py b/animism-align/cli/app/sql/env.py index af21e0e..0defa88 100644 --- a/animism-align/cli/app/sql/env.py +++ b/animism-align/cli/app/sql/env.py @@ -14,7 +14,7 @@ config.set_main_option("sqlalchemy.url", connection_url) target_metadata = Base.metadata # include the models in reverse dependency order, so relationships work -from app.sql.models.timestamp import Timestamp +from app.sql.models.annotation import Annotation from app.sql.models.paragraph import Paragraph from app.sql.models.upload import Upload diff --git a/animism-align/cli/app/sql/models/annotation.py b/animism-align/cli/app/sql/models/annotation.py new file mode 100644 index 0000000..6cc476c --- /dev/null +++ b/animism-align/cli/app/sql/models/annotation.py @@ -0,0 +1,39 @@ +from sqlalchemy import create_engine, Table, Column, Text, String, Integer, Float, DateTime, JSON, ForeignKey +from sqlalchemy.orm import relationship +import sqlalchemy.sql.functions as func +from sqlalchemy_utc import UtcDateTime, utcnow +from wtforms_alchemy import ModelForm + +from app.sql.common import db, Base, Session +# from app.sql.models.page import Page + +from app.settings import app_cfg + +class Annotation(Base): + """Table for storing references to graphs""" + __tablename__ = 'annotation' + id = Column(Integer, primary_key=True) + type = Column(String(16, convert_unicode=True), nullable=False) + paragraph_id = Column(Integer, ForeignKey('paragraph.id'), nullable=True) + start_ts = Column(Float, nullable=False) + end_ts = Column(Float, nullable=True) + text = Column(Text(convert_unicode=True), nullable=True) + settings = Column(JSON, default={}, nullable=True) + + def toJSON(self): + return { + 'id': self.id, + 'type': self.type, + 'paragraph_id': self.paragraph_id, + 'start_ts': self.start_ts, + 'end_ts': self.end_ts, + 'sentence': self.description, + 'settings': self.settings, + } + +class AnnotationForm(ModelForm): + class Meta: + model = Annotation + exclude = ['settings'] + def get_session(): + return Session() diff --git a/animism-align/cli/app/sql/models/timestamp.py b/animism-align/cli/app/sql/models/timestamp.py deleted file mode 100644 index c2bf410..0000000 --- a/animism-align/cli/app/sql/models/timestamp.py +++ /dev/null @@ -1,39 +0,0 @@ -from sqlalchemy import create_engine, Table, Column, Text, String, Integer, Float, DateTime, JSON, ForeignKey -from sqlalchemy.orm import relationship -import sqlalchemy.sql.functions as func -from sqlalchemy_utc import UtcDateTime, utcnow -from wtforms_alchemy import ModelForm - -from app.sql.common import db, Base, Session -# from app.sql.models.page import Page - -from app.settings import app_cfg - -class Timestamp(Base): - """Table for storing references to graphs""" - __tablename__ = 'timestamp' - id = Column(Integer, primary_key=True) - type = Column(String(16, convert_unicode=True), nullable=False) - paragraph_id = Column(Integer, ForeignKey('paragraph.id'), nullable=True) - start_ts = Column(Float, nullable=False) - end_ts = Column(Float, nullable=True) - sentence = Column(Text(convert_unicode=True), nullable=True) - settings = Column(JSON, default={}, nullable=True) - - def toJSON(self): - return { - 'id': self.id, - 'type': self.type, - 'paragraph_id': self.paragraph_id, - 'start_ts': self.start_ts, - 'end_ts': self.end_ts, - 'sentence': self.description, - 'settings': self.settings, - } - -class TimestampForm(ModelForm): - class Meta: - model = Timestamp - exclude = ['settings'] - def get_session(): - return Session() diff --git a/animism-align/cli/app/sql/versions/202006271347_add_paragraphs.py b/animism-align/cli/app/sql/versions/202006271347_add_paragraphs.py deleted file mode 100644 index cc134b5..0000000 --- a/animism-align/cli/app/sql/versions/202006271347_add_paragraphs.py +++ /dev/null @@ -1,56 +0,0 @@ -"""add paragraphs - -Revision ID: 650f7cdb3174 -Revises: -Create Date: 2020-06-27 13:47:33.766574 - -""" -from alembic import op -import sqlalchemy as sa -import sqlalchemy_utc - - -# revision identifiers, used by Alembic. -revision = '650f7cdb3174' -down_revision = None -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('paragraph', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('type', sa.String(length=16, _expect_unicode=True), nullable=False), - sa.Column('settings', sa.JSON(), nullable=True), - sa.PrimaryKeyConstraint('id') - ) - op.create_table('upload', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('sha256', sa.String(length=256), nullable=False), - sa.Column('fn', sa.String(length=256), nullable=False), - sa.Column('ext', sa.String(length=4, _expect_unicode=True), nullable=False), - sa.Column('username', sa.String(length=16, _expect_unicode=True), nullable=False), - sa.Column('created_at', sqlalchemy_utc.sqltypes.UtcDateTime(timezone=True), nullable=True), - sa.PrimaryKeyConstraint('id') - ) - op.create_table('timestamp', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('type', sa.String(length=16, _expect_unicode=True), nullable=False), - sa.Column('paragraph_id', sa.Integer(), nullable=True), - sa.Column('start_ts', sa.Float(), nullable=False), - sa.Column('end_ts', sa.Float(), nullable=True), - sa.Column('sentence', sa.Text(_expect_unicode=True), nullable=True), - sa.Column('settings', sa.JSON(), nullable=True), - sa.ForeignKeyConstraint(['paragraph_id'], ['paragraph.id'], ), - sa.PrimaryKeyConstraint('id') - ) - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('timestamp') - op.drop_table('upload') - op.drop_table('paragraph') - # ### end Alembic commands ### diff --git a/animism-align/cli/app/sql/versions/202007041633_create_database.py b/animism-align/cli/app/sql/versions/202007041633_create_database.py new file mode 100644 index 0000000..f8336e5 --- /dev/null +++ b/animism-align/cli/app/sql/versions/202007041633_create_database.py @@ -0,0 +1,56 @@ +"""create database + +Revision ID: f8936a84e584 +Revises: +Create Date: 2020-07-04 16:33:01.643193 + +""" +from alembic import op +import sqlalchemy as sa +import sqlalchemy_utc + + +# revision identifiers, used by Alembic. +revision = 'f8936a84e584' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('paragraph', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('type', sa.String(length=16, _expect_unicode=True), nullable=False), + sa.Column('settings', sa.JSON(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('upload', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('sha256', sa.String(length=256), nullable=False), + sa.Column('fn', sa.String(length=256), nullable=False), + sa.Column('ext', sa.String(length=4, _expect_unicode=True), nullable=False), + sa.Column('username', sa.String(length=16, _expect_unicode=True), nullable=False), + sa.Column('created_at', sqlalchemy_utc.sqltypes.UtcDateTime(timezone=True), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('annotation', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('type', sa.String(length=16, _expect_unicode=True), nullable=False), + sa.Column('paragraph_id', sa.Integer(), nullable=True), + sa.Column('start_ts', sa.Float(), nullable=False), + sa.Column('end_ts', sa.Float(), nullable=True), + sa.Column('text', sa.Text(_expect_unicode=True), nullable=True), + sa.Column('settings', sa.JSON(), nullable=True), + sa.ForeignKeyConstraint(['paragraph_id'], ['paragraph.id'], ), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('annotation') + op.drop_table('upload') + op.drop_table('paragraph') + # ### end Alembic commands ### diff --git a/animism-align/frontend/actions.js b/animism-align/frontend/actions.js index 1b5f15e..4153296 100644 --- a/animism-align/frontend/actions.js +++ b/animism-align/frontend/actions.js @@ -1,5 +1,6 @@ import { bindActionCreators } from 'redux' -import { actions as crudActions } from './api' +// import { actions as crudActions } from './api' +import { crud_actions } from './api/crud.actions' import * as audioActions from './views/audio/audio.actions' import * as alignActions from './views/align/align.actions' @@ -7,6 +8,12 @@ import * as siteActions from './views/site/site.actions' import { store } from './store' +const crudActions = [ + 'paragraph', + 'annotation', + 'upload', +].reduce((a,b) => (a[b] = crud_actions(b)) && a, {}) + export default Object.keys(crudActions) .map(a => [a, crudActions[a]]) diff --git a/animism-align/frontend/api/index.js b/animism-align/frontend/api/index.js index 96c4e5c..05f6b8b 100644 --- a/animism-align/frontend/api/index.js +++ b/animism-align/frontend/api/index.js @@ -15,9 +15,3 @@ so you can do ... */ export { util } - -export const actions = [ - 'paragraph', - 'timestamp', - 'upload', -].reduce((a,b) => (a[b] = crud_actions(b)) && a, {}) diff --git a/animism-align/frontend/store.js b/animism-align/frontend/store.js index accf1b1..24542ff 100644 --- a/animism-align/frontend/store.js +++ b/animism-align/frontend/store.js @@ -9,7 +9,7 @@ import uploadReducer from './views/upload/upload.reducer' import alignReducer from './views/align/align.reducer' import audioReducer from './views/audio/audio.reducer' import paragraphReducer from './views/paragraph/paragraph.reducer' -import timestampReducer from './views/timestamp/timestamp.reducer' +import annotationReducer from './views/annotation/annotation.reducer' import siteReducer from './views/site/site.reducer' // import collectionReducer from './views/collection/collection.reducer' @@ -22,8 +22,7 @@ const createRootReducer = history => ( align: alignReducer, audio: audioReducer, paragraph: paragraphReducer, - timestamp: timestampReducer, - // collection: collectionReducer, + annotation: annotationReducer, }) ) diff --git a/animism-align/frontend/types.js b/animism-align/frontend/types.js index d888a0a..45c9caf 100644 --- a/animism-align/frontend/types.js +++ b/animism-align/frontend/types.js @@ -5,11 +5,13 @@ export const api = crud_type('api', []) export const upload = crud_type('upload', []) export const peaks = crud_type('peaks', []) export const text = crud_type('text', []) -export const timestamp = crud_type('timestamp', []) +export const annotation = crud_type('annotation', []) export const paragraph = crud_type('paragraph', []) export const align = crud_type('align', [ 'set_display_setting', 'set_temporary_annotation', + 'update_temporary_annotation', + 'update_temporary_annotation_settings', ]) export const audio = with_type('audio', [ diff --git a/animism-align/frontend/views/align/align.actions.js b/animism-align/frontend/views/align/align.actions.js index 82e4799..b3883ae 100644 --- a/animism-align/frontend/views/align/align.actions.js +++ b/animism-align/frontend/views/align/align.actions.js @@ -25,7 +25,7 @@ export const setCursor = cursor_ts => dispatch => ( dispatch({ type: types.align.set_display_setting, key: 'cursor_ts', value: cursor_ts }) ) -export const showNewTimestampForm = (start_ts, text) => dispatch => { +export const showNewAnnotationForm = (start_ts, text) => dispatch => { let croppedText; if (store.getState().align.annotation.start_ts) { croppedText = store.getState().align.annotation.text @@ -44,6 +44,13 @@ export const showNewTimestampForm = (start_ts, text) => dispatch => { }) } +export const updateAnnotationForm = (key, value) => dispatch => { + dispatch({ type: types.align.update_temporary_annotation, key, value }) +} +export const updateAnnotationSettings = (key, value) => dispatch => { + dispatch({ type: types.align.update_temporary_annotation_settings, key, value }) +} + const cutFirstSentence = text => { const textToCrop = text.trim().replace("\n", " ").split("\n")[0] let cropIndex = textToCrop.indexOf('. ') + 1 @@ -54,7 +61,7 @@ const cutFirstSentence = text => { return croppedText } -export const hideTimestampForm = () => dispatch => { +export const hideAnnotationForm = () => dispatch => { dispatch({ type: types.align.set_temporary_annotation, data: {} diff --git a/animism-align/frontend/views/align/align.reducer.js b/animism-align/frontend/views/align/align.reducer.js index 9064b56..f080c24 100644 --- a/animism-align/frontend/views/align/align.reducer.js +++ b/animism-align/frontend/views/align/align.reducer.js @@ -41,6 +41,27 @@ export default function alignReducer(state = initialState, action) { annotation: action.data, } + case types.align.update_temporary_annotation: + return { + ...state, + annotation: { + ...state.annotation, + [action.key]: action.value, + } + } + + case types.align.update_temporary_annotation_settings: + return { + ...state, + annotation: { + ...state.annotation, + settings: { + ...state.annotation.settings, + [action.key]: action.value, + } + } + } + default: return state } diff --git a/animism-align/frontend/views/align/components/annotations/annotation.form.js b/animism-align/frontend/views/align/components/annotations/annotation.form.js index 03e44e1..9432948 100644 --- a/animism-align/frontend/views/align/components/annotations/annotation.form.js +++ b/animism-align/frontend/views/align/components/annotations/annotation.form.js @@ -11,36 +11,42 @@ import { clamp } from '../../../../util' import { timeToPosition } from '../../align.util' import { Select } from '../../../../common' -const TIMESTAMP_TYPES = ['sentence', 'header'].map(name => ({ name, label: name })) +const ANNOTATION_TYPES = [ + 'sentence', 'header' +].map(name => ({ name, label: name })) class AnnotationForm extends Component { - state = { - data: {}, - } constructor(props){ super(props) this.handleChange = this.handleChange.bind(this) this.handleSelect = this.handleSelect.bind(this) this.handleKeyDown = this.handleKeyDown.bind(this) - } - componentDidMount(){ - this.setState({ - data: { ...this.props.annotation }, - }) - } - componentDidUpdate(prevProps){ - if (this.props.annotation !== prevProps.annotation) { - this.setState({ - data: { - ...this.props.annotation, - text: this.state.data.text, - type: this.state.data.text, - }, - }) - } + this.handleSubmit = this.handleSubmit.bind(this) } handleKeyDown(e) { + if (!e.metaKey && !e.ctrlKey) return + let { start_ts } = this.props.annotation + console.log(e.keyCode) switch (e.keyCode) { + case 38: // up + e.preventDefault() + start_ts -= 0.1 + actions.align.updateAnnotationForm('start_ts', start_ts) + actions.audio.seek(start_ts) + actions.align.setCursor(start_ts) + break + case 40: // down + e.preventDefault() + start_ts += 0.1 + actions.align.updateAnnotationForm('start_ts', start_ts) + actions.audio.seek(start_ts) + actions.align.setCursor(start_ts) + break + case 83: // ctrl-S + e.preventDefault() + this.handleSubmit() + default: + break } } handleChange(e) { @@ -48,62 +54,57 @@ class AnnotationForm extends Component { this.handleSelect(name, value) } handleSelect(name, value) { - this.setState({ - data: { - ...this.state.data, - [name]: value, - } - }) + actions.align.updateAnnotationForm(name, value) } handleSubmit() { - const { data } = this.state - if (data.id === 'new') { - delete data.id - actions.graph.create(data) + const { annotation } = this.props + if (annotation.id === 'new') { + delete annotation.id + actions.annotation.create(annotation) .then(response => { console.log(response) - actions.align.hideTimestampForm() + actions.align.hideAnnotationForm() }) } else { - actions.graph.update(data) + actions.annotation.update(annotation) .then(response => { console.log(response) - actions.align.hideTimestampForm() + actions.align.hideAnnotationForm() }) } } render() { - const { timeline } = this.props - const { data } = this.state - if (!data.start_ts) return
+ const { timeline, annotation } = this.props + if (!annotation.start_ts) return
return (
- {data.type === 'sentence' && this.renderTextarea()} - {data.type === 'header' && this.renderTextarea()} + {annotation.type === 'sentence' && this.renderTextarea()} + {annotation.type === 'header' && this.renderTextarea()}