Skip to content

Commit 7f107ed

Browse files
authored
Merge pull request #1852 from codidact/0valt/tags
Improve tag rename error handling & messages
2 parents 6a63157 + b1f86f6 commit 7f107ed

File tree

8 files changed

+119
-57
lines changed

8 files changed

+119
-57
lines changed

app/assets/javascripts/qpixel_api.js

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -430,19 +430,34 @@ window.QPixel = {
430430
},
431431

432432
handleJSONResponse: (data, onSuccess, onFinally) => {
433-
const is_modified = data.status === 'modified';
434-
const is_success = data.status === 'success';
433+
const isFailed = data.status === 'failed';
435434

436-
if (is_modified || is_success) {
437-
onSuccess(/** @type {Parameters<typeof onSuccess>[0]} */(data));
435+
if(isFailed) {
436+
const { errors = [], message } = data;
437+
438+
if (message) {
439+
const fullMessage =
440+
errors.length > 1
441+
? `${message}:<ul>${errors.map((e) => `<li>${e.trim()}</li>`).join('')}</ul>`
442+
: errors.length === 1
443+
? `${message} (${errors[0].toLowerCase().trim()})`
444+
: message;
445+
446+
QPixel.createNotification('danger', fullMessage);
447+
}
448+
else {
449+
for (const error of errors) {
450+
QPixel.createNotification('danger', error);
451+
}
452+
}
438453
}
439454
else {
440-
QPixel.createNotification('danger', data.message);
455+
onSuccess(/** @type {Parameters<typeof onSuccess>[0]} */(data));
441456
}
442457

443458
onFinally?.(data);
444459

445-
return is_success;
460+
return !isFailed;
446461
},
447462

448463
flag: async (flag) => {

app/assets/javascripts/tags.js

Lines changed: 26 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,4 @@
1-
/**
2-
* @typedef {{
3-
* id: number | string
4-
* text: string
5-
* desc: string
6-
* synonyms?: string | QPixelTagSynonym[]
7-
* }} ProcessedTag
8-
*/
9-
10-
$(() => {
1+
document.addEventListener('DOMContentLoaded', () => {
112
const sum = (/** @type {number[]} */ ary) => ary.reduce((a, b) => a + b, 0);
123

134
/**
@@ -35,11 +26,11 @@ $(() => {
3526
const tagSynonyms = !!tag.synonyms ? ` <i>(${tag.synonyms})</i>` : '';
3627
const tagSpan = `<span>${tag.text}${tagSynonyms}</span>`;
3728
let desc = !!tag.desc ? splitWordsMaxLength(tag.desc, 120) : '';
38-
const descSpan = !!tag.desc ?
39-
`<br/><span class="has-color-tertiary-900 has-font-size-caption">${desc[0]}${desc.length > 1 ? '...' : ''}</span>` :
40-
'';
29+
const descSpan = !!tag.desc
30+
? `<br/><span class="has-color-tertiary-900 has-font-size-caption">${desc[0]}${desc.length > 1 ? '...' : ''}</span>`
31+
: '';
4132
return $(tagSpan + descSpan);
42-
}
33+
};
4334

4435
$('.js-tag-select').each((_i, el) => {
4536
const $tgt = $(el);
@@ -49,10 +40,10 @@ $(() => {
4940
tags: $tgt.attr('data-create') !== 'false',
5041
/**
5142
* @param {Select2.IdTextPair[]} data
52-
* @param {Select2.IdTextPair & { desc?: string }} tag
43+
* @param {Select2.IdTextPair & { desc?: string }} tag
5344
*/
5445
insertTag: function (data, tag) {
55-
tag.desc = "(Create new tag)"
46+
tag.desc = '(Create new tag)';
5647
// Insert the tag at the end of the results
5748
data.push(tag);
5849
},
@@ -66,7 +57,7 @@ $(() => {
6657
}
6758
return Object.assign(params, { tag_set: $this.data('tag-set') });
6859
},
69-
headers: { 'Accept': 'application/json' },
60+
headers: { Accept: 'application/json' },
7061
delay: 100,
7162
/**
7263
* @param {QPixelTag[]} data
@@ -80,23 +71,23 @@ $(() => {
8071
{ id: 1, text: 'hot-red-firebreather', desc: 'Very cute dragon' },
8172
{ id: 2, text: 'training', desc: 'How to train a dragon' },
8273
{ id: 3, text: 'behavior', desc: 'How a dragon behaves' },
83-
{ id: 4, text: 'sapphire-blue-waterspouter', desc: 'Other cute dragon' }
84-
]
85-
}
74+
{ id: 4, text: 'sapphire-blue-waterspouter', desc: 'Other cute dragon' },
75+
],
76+
};
8677
}
8778
return {
8879
results: data.map((t) => ({
8980
id: useIds ? t.id : t.name,
9081
text: t.name.replace(/</g, '&#x3C;').replace(/>/g, '&#x3E;'),
9182
synonyms: processSynonyms($this, t.tag_synonyms),
92-
desc: t.excerpt
93-
}))
83+
desc: t.excerpt,
84+
})),
9485
};
9586
},
9687
},
9788
placeholder: '',
9889
templateResult: template,
99-
allowClear: true
90+
allowClear: true,
10091
});
10192
});
10293

@@ -112,10 +103,13 @@ $(() => {
112103
if (synonyms.length > 3) {
113104
const searchValue = $search.data('select2').selection.$search.val().toLowerCase();
114105
displayedSynonyms = synonyms.filter((ts) => ts.name.includes(searchValue)).slice(0, 3);
115-
} else {
106+
}
107+
else {
116108
displayedSynonyms = synonyms;
117109
}
118-
let synonymsString = displayedSynonyms.map((ts) => `${ts.name.replace(/</g, '&#x3C;').replace(/>/g, '&#x3E;')}`).join(', ');
110+
let synonymsString = displayedSynonyms
111+
.map((ts) => `${ts.name.replace(/</g, '&#x3C;').replace(/>/g, '&#x3E;')}`)
112+
.join(', ');
119113
if (synonyms.length > displayedSynonyms.length) {
120114
synonymsString += `, ${synonyms.length - displayedSynonyms.length} more synonyms`;
121115
}
@@ -128,10 +122,10 @@ $(() => {
128122
const newId = parseInt(lastId, 10) + 1;
129123

130124
//Duplicate the first element at the end of the wrapper
131-
const newField = $wrapper.find('.tag-synonym[data-id="0"]')[0]
132-
.outerHTML
133-
.replace(/data-id="0"/g, 'data-id="' + newId + '"')
134-
.replace(/(?<connector>attributes(\]\[)|(_))0/g, '$<connector>' + newId)
125+
const newField = $wrapper
126+
.find('.tag-synonym[data-id="0"]')[0]
127+
.outerHTML.replace(/data-id="0"/g, 'data-id="' + newId + '"')
128+
.replace(/(?<connector>attributes(\]\[)|(_))0/g, '$<connector>' + newId);
135129
$wrapper.append(newField);
136130

137131
//Alter the newly added tag synonym
@@ -141,10 +135,10 @@ $(() => {
141135
$newTagSynonym.show();
142136

143137
//Add handler for removing an element
144-
$newTagSynonym.find(`.remove-tag-synonym`).click(removeTagSynonym);
138+
$newTagSynonym.find(`.remove-tag-synonym`).on('click', removeTagSynonym);
145139
});
146140

147-
$('.remove-tag-synonym').click(removeTagSynonym);
141+
$('.remove-tag-synonym').on('click', removeTagSynonym);
148142

149143
function removeTagSynonym() {
150144
const synonym = $(this).closest('.tag-synonym');
@@ -174,7 +168,7 @@ $(() => {
174168
const tagId = $tgt.attr('data-tag');
175169
const tagName = $tgt.attr('data-name');
176170

177-
const renameTo = prompt(`Rename tag ${tagName} to:`);
171+
const renameTo = prompt(`Rename tag "${tagName}" to:`);
178172

179173
if (!renameTo) {
180174
return;

app/controllers/tags_controller.rb

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,10 +94,8 @@ def new
9494
def create
9595
@tag = Tag.new(tag_params.merge(tag_set_id: @category.tag_set.id))
9696
if @tag.save
97-
flash[:danger] = nil
9897
redirect_to tag_path(id: @category.id, tag_id: @tag.id)
9998
else
100-
flash[:danger] = @tag.errors.full_messages.join(', ')
10199
render :new, status: :bad_request
102100
end
103101
end
@@ -156,6 +154,7 @@ def rename
156154
else
157155
render json: { status: 'failed',
158156
message: I18n.t('tags.errors.rename_generic'),
157+
errors: @tag.errors.full_messages,
159158
tag: @tag },
160159
status: :bad_request
161160
end

app/models/tag.rb

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,21 @@ class Tag < ApplicationRecord
1212

1313
validates :excerpt, length: { maximum: 600 }, allow_blank: true
1414
validates :wiki_markdown, length: { maximum: 30_000 }, allow_blank: true
15-
validates :name, presence: true, format: { with: /[^ \t]+/, message: 'Tag names may not include spaces' }
15+
validates :name, presence: {
16+
message: I18n.t('tags.validation.errors.no_blank_name')
17+
}
18+
validates :name, format: {
19+
with: /\A[^\s]+\Z/, # https://regex101.com/r/7BxgIn/1
20+
message: I18n.t('tags.validation.errors.no_spaces_in_name')
21+
}
1622
validate :parent_not_self
1723
validate :parent_not_own_child
1824
validate :synonym_unique
19-
validates :name, uniqueness: { scope: [:tag_set_id], case_sensitive: false }
25+
validates :name, uniqueness: {
26+
scope: [:tag_set_id],
27+
case_sensitive: false,
28+
message: I18n.t('tags.validation.errors.no_duplicate_names')
29+
}
2030

2131
# scopes
2232
scope :list_includes, -> { includes(:tag_synonyms) }

config/locales/strings/en.tags.yml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
en:
22
tags:
3+
validation:
4+
errors:
5+
no_blank_name: >
6+
should contain at least 1 character
7+
no_duplicate_names: >
8+
cannot match an already existing tag
9+
no_spaces_in_name: >
10+
should not contain spaces
311
errors:
412
rename_generic: >
5-
Failed to rename the tag.
13+
Failed to rename the tag

global.d.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@ interface PostValidatorMessage {
1212

1313
type PostValidator = (postText: string) => [boolean, PostValidatorMessage[]];
1414

15+
interface ProcessedTag {
16+
id: number | string
17+
text: string
18+
desc: string
19+
synonyms?: string | QPixelTagSynonym[]
20+
}
21+
1522
interface UserPreferences {
1623
community: Record<string, string | null>;
1724
global: Record<string, string | null>;

0 commit comments

Comments
 (0)