Skip to content

Commit 16de2c0

Browse files
authored
Make entry content an array content[]. This is a major breaking change. (#55)
This patch refactors the `entries.content` table field from TEXT to TEXT[] to support multiple content values per entry (e.g., alternative spellings, transliterations, or script variations of the same word). This was added after working with the Kurup Malayalam thesaurus, where each "definition" entry is actually a series of synonyms. Rather than joining everything with a comma and dumping in `content`, it's better to provision for multiple values. - Break to version v4.0.0 - Database schema: `content TEXT` to `content TEXT[]` - APIs: `content` is now an array in all JSON responses and request payloads - Change Admin UI to add multi-textareas for content.
1 parent 9d00bab commit 16de2c0

File tree

17 files changed

+134
-58
lines changed

17 files changed

+134
-58
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ build: $(BIN)
1818
$(STUFFBIN):
1919
go install github.com/knadh/stuffbin/...
2020

21-
$(BIN): $(shell find . -type f -name "*.go")
21+
$(BIN): $(shell find . -type f -name "*.go") ${STATIC}
2222
CGO_ENABLED=0 go build -o ${BIN} -ldflags="-s -w -X 'main.buildString=${BUILDSTR}' -X 'main.versionString=${VERSION}'" cmd/${BIN}/*.go
2323

2424
.PHONY: run

admin/definition.html

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,21 @@
77
<div class="panel relation-form">
88
<h3>Add definition</h3>
99
<form @submit.prevent="onSave">
10-
<div x-text="parent.content"></div>
10+
<div>
11+
<template x-for="(item, idx) in parent.content" :key="idx">
12+
<span x-text="item" style="display: block;"></span>
13+
</template>
14+
</div>
1115
<div>&darr;</div>
1216
<div>
1317
<label>Definition</label>
14-
<textarea required autofocus name="content" x-ref="content" x-model="def.content"></textarea>
18+
<template x-for="(item, idx) in def.content" :key="idx">
19+
<div style="margin-bottom: 0.5rem;">
20+
<textarea required x-model="def.content[idx]" x-ref="contentInput" :autofocus="idx === 0" style="width: calc(100% - 60px); display: inline-block;"></textarea>
21+
<a href="#" @click.prevent="onDeleteDefContentItem(idx)" x-show="def.content.length > 1" style="margin-left: 0.5rem;">Delete</a>
22+
</div>
23+
</template>
24+
<a href="#" @click.prevent="onAddDefContentItem">+ Add</a>
1525
</div>
1626
<br />
1727
<template x-if="Object.keys(config.languages[parent.lang].types).length > 0">

admin/entry.html

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,13 @@ <h3 x-text="isNew ? 'New entry' : 'Edit entry'"></h3>
1111
<div class="row">
1212
<div class="column nine">
1313
<label>Content</label>
14-
<textarea required autofocus name="content" x-ref="content" x-model="entry.content"></textarea>
14+
<template x-for="(item, idx) in entry.content" :key="idx">
15+
<div style="margin-bottom: 0.5rem;">
16+
<textarea required x-model="entry.content[idx]" x-ref="contentInput" :autofocus="idx === 0"></textarea>
17+
<a href="#" @click.prevent="onDeleteContentItem(idx)" x-show="entry.content.length > 1" class="text-xsmall float-right">Delete</a>
18+
</div>
19+
</template>
20+
<a href="#" @click.prevent="onAddContentItem">+ Add</a>
1521
</div>
1622
<div class="column three">
1723
<label>Initial</label>
@@ -106,7 +112,11 @@ <h3>Parent entries (<span x-text="parentEntries.length"></span>)</h3>
106112
<template x-for="r in parentEntries" :key="r.id">
107113
<li class="rel">
108114
<p>
109-
<a x-bind:href="makeURL({'id': r.id})" x-text="r.content" class="content"></a>
115+
<a x-bind:href="makeURL({'id': r.id})" class="content">
116+
<template x-for="(item, idx) in r.content" :key="idx">
117+
<span><span x-text="item"></span><span x-show="idx < r.content.length - 1">, </span></span>
118+
</template>
119+
</a>
110120
<template x-if="r.phones.length > 0">
111121
<span class="phones" x-text="r.phones.join(', ')"></span>
112122
</template>

admin/pending.html

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,11 @@ <h3><span x-text="total"></span> pending entries
2424
<span class="tag new">New word</span>
2525
</template>
2626
<h3>
27-
<a x-bind:href="makeURL({id: e.id})" @click.prevent="onEditEntry(e)" x-text="e.content"></a>
27+
<a x-bind:href="makeURL({id: e.id})" @click.prevent="onEditEntry(e)">
28+
<template x-for="(item, idx) in e.content" :key="idx">
29+
<span><span x-text="item"></span><span x-show="idx < e.content.length - 1">, </span></span>
30+
</template>
31+
</a>
2832
</h3>
2933
<sup class="lang meta" x-text="config.languages[e.lang].name"></sup>
3034

@@ -50,7 +54,12 @@ <h3>
5054
<span class="tag" :class="{ [r.relation.status]: true }" x-text="r.relation.status"></span>
5155
</template>
5256
<span class="meta lang" x-text="config.languages[r.lang].name"></span>
53-
<span class="meta types" x-text="r.relation.types"></span> <span x-text="r.content" class="content"></span>
57+
<span class="meta types" x-text="r.relation.types"></span>
58+
<span class="content">
59+
<template x-for="(item, idx) in r.content" :key="idx">
60+
<span><span x-text="item"></span><span x-show="idx < r.content.length - 1">, </span></span>
61+
</template>
62+
</span>
5463
</p>
5564
<template x-if="e.id in comments && r.id in comments[e.id]">
5665
<template x-for="(c, n) in comments[e.id][r.id]" :key="c.id">

admin/search.html

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,11 @@ <h3><span x-text="total"></span> results for &ldquo;<span x-text="query"></span>
1919

2020
<div class="heading">
2121
<h3>
22-
<a x-bind:href="makeURL({id: e.id})" @click.prevent="onEditEntry(e)" x-text="e.content"></a>
22+
<a x-bind:href="makeURL({id: e.id})" @click.prevent="onEditEntry(e)">
23+
<template x-for="(item, idx) in e.content" :key="idx">
24+
<span><span x-text="item"></span><span x-show="idx < e.content.length - 1">, </span></span>
25+
</template>
26+
</a>
2327
</h3>
2428
<sup class="lang" x-text="config.languages[e.lang].name"></sup>
2529
</div>
@@ -30,7 +34,12 @@ <h3>
3034
<li class="rel" :class="{ highlight: order[e.id] && order[e.id].changedRels[r.id] }">
3135
<p>
3236
<span class="meta lang" x-text="config.languages[r.lang].name"></span>
33-
<span class="meta types" x-text="r.relation.types"></span> <span x-text="r.content" class="content"></span>
37+
<span class="meta types" x-text="r.relation.types"></span>
38+
<span class="content">
39+
<template x-for="(item, idx) in r.content" :key="idx">
40+
<span><span x-text="item"></span><span x-show="idx < r.content.length - 1">, </span></span>
41+
</template>
42+
</span>
3443
</p>
3544
<div class="actions">
3645
<a href="#" @click.prevent="onDetatchRelation(e.id, r.relation.id)">Detatch</a>

admin/static/main.js

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ function globalComponent() {
9696

9797
onNewEntry() {
9898
this.$dispatch('open-entry-form', {
99+
content: [''],
99100
weight: 0,
100101
lang: Object.keys(this.config.languages)[0],
101102
tokens: '',
@@ -349,6 +350,7 @@ function entryComponent() {
349350
const data = e.detail;
350351
this.entry = {
351352
...data,
353+
content: Array.isArray(data.content) ? [...data.content] : [data.content || ''],
352354
phones: data.phones.join('\n'),
353355
tags: data.tags.join('\n'),
354356
tokens: data.tokens.split(' ').join('\n'),
@@ -358,23 +360,29 @@ function entryComponent() {
358360
this.isNew = !this.entry.id ? true : false;
359361
this.isVisible = true;
360362

361-
this.$nextTick(() => {
362-
this.$refs.content.focus();
363-
});
364-
365363
if (this.entry.parent) {
366364
this.getParentEntries(this.entry.id);
367365
}
368366
},
369367

368+
onAddContentItem() {
369+
this.entry.content.push('');
370+
},
371+
372+
onDeleteContentItem(idx) {
373+
if (this.entry.content.length > 1) {
374+
this.entry.content.splice(idx, 1);
375+
}
376+
},
377+
370378
onToggleOptions() {
371379
this.isFormOpen = !this.isFormOpen;
372380
localStorage.isFormOpen = this.isFormOpen;
373381
},
374382

375383
onFocusInitial() {
376-
if (!this.entry.initial && this.entry.content && this.entry.content.length > 0) {
377-
this.entry.initial = this.entry.content[0].toUpperCase();
384+
if (!this.entry.initial && Array.isArray(this.entry.content) && this.entry.content.length > 0 && this.entry.content[0].length > 0) {
385+
this.entry.initial = this.entry.content[0][0].toUpperCase();
378386
}
379387
},
380388

@@ -398,7 +406,8 @@ function entryComponent() {
398406

399407
let data = {
400408
...this.entry,
401-
initial: this.entry.initial ? this.entry.initial : this.entry.content[0].toUpperCase(),
409+
content: this.entry.content.filter(c => c.trim() !== ''),
410+
initial: this.entry.initial ? this.entry.initial : (this.entry.content.length > 0 && this.entry.content[0].length > 0 ? this.entry.content[0][0].toUpperCase() : ''),
402411
phones: linesToList(this.entry.phones),
403412
tags: linesToList(this.entry.tags),
404413
tokens: linesToList(this.entry.tokens).join(' ')
@@ -498,17 +507,28 @@ function definitionComponent() {
498507
this.parent = e.detail.parent;
499508
delete (e.detail.parent);
500509

501-
this.def = { ...e.detail, tags: e.detail.tags.join('\n') };
510+
this.def = {
511+
...e.detail,
512+
content: [''],
513+
tags: e.detail.tags.join('\n')
514+
};
502515
this.isVisible = true;
503-
this.$nextTick(() => {
504-
this.$refs.content.focus();
505-
});
516+
},
517+
518+
onAddDefContentItem() {
519+
this.def.content.push('');
520+
},
521+
522+
onDeleteDefContentItem(idx) {
523+
if (this.def.content.length > 1) {
524+
this.def.content.splice(idx, 1);
525+
}
506526
},
507527

508528
onSave() {
509529
const params = {
510-
content: this.def.content,
511-
initial: this.def.content[0].toUpperCase(),
530+
content: this.def.content.filter(c => c.trim() !== ''),
531+
initial: this.def.content.length > 0 && this.def.content[0].length > 0 ? this.def.content[0][0].toUpperCase() : '',
512532
lang: this.def.lang,
513533
phones: [],
514534
tags: [],

admin/static/style.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,10 @@ section {
140140
margin-bottom: 45px;
141141
}
142142

143+
.text-xsmall {
144+
font-size: 0.775rem;
145+
}
146+
143147
.box {
144148
border: 1px solid #ddd;
145149
border-radius: 3px;

cmd/dictpress/admin.go

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ func handleInsertEntry(c echo.Context) error {
5757
fmt.Sprintf("error parsing request: %v", err))
5858
}
5959

60-
if err := validateEntry(e, app); err != nil {
60+
e, err := validateEntry(e, app)
61+
if err != nil {
6162
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
6263
}
6364

@@ -183,6 +184,11 @@ func handleUpdateEntry(c echo.Context) error {
183184
fmt.Sprintf("error parsing request: %v", err))
184185
}
185186

187+
e, err := validateEntry(e, app)
188+
if err != nil {
189+
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
190+
}
191+
186192
if err := app.data.UpdateEntry(id, e); err != nil {
187193
return echo.NewHTTPError(http.StatusInternalServerError,
188194
fmt.Sprintf("error updating entry: %v", err))
@@ -390,20 +396,24 @@ func handleDeletePending(c echo.Context) error {
390396
return c.JSON(http.StatusOK, okResp{true})
391397
}
392398

393-
func validateEntry(e data.Entry, app *App) error {
394-
if strings.TrimSpace(e.Content) == "" {
395-
return errors.New("invalid `content`.")
399+
func validateEntry(e data.Entry, app *App) (data.Entry, error) {
400+
for i, v := range e.Content {
401+
e.Content[i] = strings.TrimSpace(v)
402+
}
403+
404+
if len(e.Content) == 0 || strings.TrimSpace(e.Content[0]) == "" {
405+
return data.Entry{}, errors.New("invalid `content`")
396406
}
397407

398408
if strings.TrimSpace(e.Initial) == "" {
399-
return errors.New("invalid `initial`.")
409+
return data.Entry{}, errors.New("invalid `initial`")
400410
}
401411

402412
if _, ok := app.data.Langs[e.Lang]; !ok {
403-
return errors.New("unknown `lang`.")
413+
return data.Entry{}, errors.New("unknown `lang`")
404414
}
405415

406-
return nil
416+
return e, nil
407417
}
408418

409419
// handleAdminPage is the root handler that renders the Javascript admin frontend.

cmd/dictpress/submit.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ func handleNewSubmission(c echo.Context) error {
8888
e := data.Entry{
8989
Lang: s.EntryLang,
9090
Initial: strings.ToUpper(string(s.EntryContent[0])),
91-
Content: s.EntryContent,
91+
Content: pq.StringArray([]string{s.EntryContent}),
9292
Phones: pq.StringArray(phones),
9393
Tags: pq.StringArray{},
9494
Status: data.StatusPending,
@@ -113,8 +113,8 @@ func handleNewSubmission(c echo.Context) error {
113113

114114
toID, err := app.data.InsertSubmissionEntry(data.Entry{
115115
Lang: s.RelationLang[i],
116-
Initial: strings.ToUpper(string(s.RelationContent[0][0])),
117-
Content: s.RelationContent[i],
116+
Initial: strings.ToUpper(string(s.RelationContent[i][0])),
117+
Content: pq.StringArray([]string{s.RelationContent[i]}),
118118
Phones: pq.StringArray(phones),
119119
Tags: pq.StringArray{},
120120
Status: data.StatusPending,

cmd/dictpress/upgrade.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ type migFunc struct {
2626
// The functions are named as: v0.7.0 => migrations.V0_7_0() and are idempotent.
2727
var migList = []migFunc{
2828
{"v2.0.0", migrations.V2_0_0},
29+
{"v4.0.0", migrations.V4_0_0},
2930
}
3031

3132
// upgrade upgrades the database to the current version by running SQL migration files

0 commit comments

Comments
 (0)