Jump to content

Module:Feedback: Difference between revisions

From The Petit Planet Wiki
ReisuDesign (talk | contribs)
Replaced content with "------------------------------------------------------------ -- Resolve control ------------------------------------------------------------ local function renderResolveControl( parent, id, compact, page ) if not id or id == '' then return end local control = parent:tag('span') :addClass('feedback-resolve-button') :attr( 'data-feedback-id', id ) if page and page ~= '' then control:attr( 'data-feedback-page', page ) end if..."
Tag: Replaced
ReisuDesign (talk | contribs)
No edit summary
Line 1: Line 1:
local p = {}
------------------------------------------------------------
-- Helpers
------------------------------------------------------------
local function isTruthy(value)
if value == nil then
return false
end
value =
mw.text.trim(
tostring(value)
):lower()
return value ~= ''
and value ~= '0'
and value ~= 'false'
and value ~= 'no'
and value ~= 'off'
end
local function trim(value)
return mw.text.trim(
tostring(value or '')
)
end
local function formatUser(user)
user =
trim(user)
if user == ''
or user:lower() == 'anonymous'
then
return 'Anonymous'
end
return string.format(
'[[User:%s|%s]]',
user,
user
)
end
local function currentSubjectPage()
local title =
mw.title.getCurrentTitle()
return title.subjectPageTitle.fullText
end
local function getBucket()
if not (mw.ext and mw.ext.bucket) then
return nil, 'Bucket extension is not available'
end
local bucket =
mw.ext.bucket('feedback')
if not bucket then
return nil, 'Feedback bucket could not be opened'
end
return bucket
end
local function sortNewestFirst(results)
if not results then
return {}
end
table.sort(
results,
function(a, b)
local aDate =
trim(a.createddate)
local bDate =
trim(b.createddate)
if aDate == ''
and bDate ~= ''
then
return false
end
if aDate ~= ''
and bDate == ''
then
return true
end
if aDate ~= bDate then
return aDate > bDate
end
return trim(a.id) > trim(b.id)
end
)
return results
end
local function queryFeedback(
page,
status
)
local bucket, err =
getBucket()
if not bucket then
return nil, err
end
local query =
bucket.select(
'page',
'user',
'status',
'feedback',
'id',
'createddate',
'link',
'image',
'resolvedby',
'resolveddate'
)
if page and page ~= '' then
query.where(
'page',
page
)
end
if status
and status ~= ''
and status ~= 'all'
then
query.where(
'status',
status
)
end
local success, results =
pcall(
function()
return query.run()
end
)
if not success then
return nil, tostring(results)
end
return sortNewestFirst(
results or {}
)
end
------------------------------------------------------------
-- Save feedback
------------------------------------------------------------
local function saveFeedback(data)
local bucket, err =
getBucket()
if not bucket then
return false, err
end
local success, saveError =
pcall(
function()
bucket.put({
page =
data.page,
user =
data.user,
status =
data.status,
feedback =
data.feedback,
id =
data.id,
createddate =
data.createddate,
link =
data.link,
image =
data.image,
resolvedby =
data.resolvedby,
resolveddate =
data.resolveddate
})
end
)
if not success then
return false, tostring(saveError)
end
return true
end
------------------------------------------------------------
-- Shared metadata
------------------------------------------------------------
local function renderMetadata(
root,
entry
)
local meta =
root:tag('div')
:addClass(
'feedback-meta'
)
if entry.user
and entry.user ~= ''
then
meta:tag('span')
:addClass(
'feedback-user'
)
:wikitext(
formatUser(
entry.user
)
)
end
if entry.createddate
and entry.createddate ~= ''
then
meta:tag('span')
:addClass(
'feedback-meta-date'
)
:wikitext(
' · ' ..
mw.text.nowiki(
entry.createddate
)
)
end
end
------------------------------------------------------------
-- Attachments
------------------------------------------------------------
local function renderAttachments(
root,
entry
)
local link =
trim(entry.link)
local image =
trim(entry.image)
if link == ''
and image == ''
then
return
end
local attachments =
root:tag('div')
:addClass(
'feedback-attachments'
)
if link ~= '' then
attachments:tag('span')
:addClass(
'feedback-attachment'
)
:wikitext(
'🔗 ' ..
'[' ..
link ..
' Link]'
)
end
if image ~= '' then
local imageAttachment =
attachments:tag('span')
:addClass(
'feedback-attachment'
)
imageAttachment:tag('span')
:addClass(
'feedback-image-icon'
)
:wikitext('▣')
imageAttachment:tag('span')
:addClass(
'feedback-image-link'
)
:wikitext(
'[[Media:' ..
image ..
'|Picture]]'
)
end
end
------------------------------------------------------------
-- Resolution information
------------------------------------------------------------
local function renderResolution(
root,
entry
)
local resolvedBy =
trim(entry.resolvedby)
local resolvedDate =
trim(entry.resolveddate)
if resolvedBy == ''
and resolvedDate == ''
then
return
end
local resolution =
root:tag('div')
:addClass(
'feedback-resolution'
)
resolution:wikitext(
'↳ Resolved by '
)
if resolvedBy ~= '' then
resolution:wikitext(
formatUser(
resolvedBy
)
)
else
resolution:wikitext(
'Unknown'
)
end
if resolvedDate ~= '' then
resolution:wikitext(
' on ' ..
mw.text.nowiki(
resolvedDate
)
)
end
end
------------------------------------------------------------
------------------------------------------------------------
-- Resolve control
-- Resolve control
Line 6: Line 477:
parent,
parent,
id,
id,
compact,
page,
page
compact
)
)


if not id or id == '' then
if not id
or id == ''
then
return
return
end
end


local control =
local control =
parent:tag('span')
parent:tag('span')
:addClass('feedback-resolve-button')
:addClass(
:attr(
'feedback-resolve-button'
'data-feedback-id',
id
)
)


if page and page ~= '' then
 
 
control:attr(
'data-feedback-id',
id
)
 
 
if page
and page ~= ''
then
 
control:attr(
control:attr(
'data-feedback-page',
'data-feedback-page',
page
page
)
)
end
end


if compact then
if compact then


control
control
:addClass('feedback-resolve-button--compact')
:addClass(
:attr('aria-label', 'Resolve feedback')
'feedback-resolve-button--compact'
:attr('title', 'Resolve feedback')
)
:wikitext('✓')
:attr(
'role',
'button'
)
:attr(
'tabindex',
'0'
)
:attr(
'aria-label',
'Resolve feedback'
)
:attr(
'title',
'Resolve feedback'
)
:wikitext(
'✓'
)


else
else


control
control
:attr('role', 'button')
:attr(
:attr('tabindex', '0')
'role',
:attr('aria-label', 'Resolve feedback')
'button'
:wikitext('Resolve')
)
:attr(
'tabindex',
'0'
)
:attr(
'aria-label',
'Resolve feedback'
)
:wikitext(
'Resolve'
)
 
end
 
end
 
 
------------------------------------------------------------
-- Full feedback card
------------------------------------------------------------
 
local function renderFullCard(
entry
)
 
local resolved =
entry.status == 'resolved'
 
 
local id =
trim(entry.id)
 
 
local page =
trim(entry.page)
 
 
local root =
mw.html.create('div')
:addClass(
'feedback-box'
)
:addClass(
resolved
and 'feedback-box--resolved'
or 'feedback-box--unresolved'
)
 
 
if id ~= '' then
 
root:attr(
'data-feedback-id',
id
)
 
end
 
 
--------------------------------------------------------
-- Main line
--------------------------------------------------------
 
local main =
root:tag('div')
:addClass(
'feedback-main-line'
)
 
 
main:tag('span')
:addClass(
'feedback-status'
)
:addClass(
resolved
and 'feedback-status--resolved'
or 'feedback-status--unresolved'
)
:wikitext(
resolved
and 'RESOLVED'
or 'UNRESOLVED'
)
 
 
main:tag('span')
:addClass(
'feedback-text'
)
:wikitext(
entry.feedback
or 'No feedback provided'
)
 
 
--------------------------------------------------------
-- Inline metadata
--------------------------------------------------------
 
local metadata =
main:tag('span')
:addClass(
'feedback-inline-meta'
)
 
 
if entry.user
and entry.user ~= ''
then
 
metadata:wikitext(
' · ' ..
formatUser(
entry.user
)
)
 
end
 
 
if entry.createddate
and entry.createddate ~= ''
then
 
metadata:wikitext(
' ' ..
mw.text.nowiki(
entry.createddate
)
)
 
end
 
 
--------------------------------------------------------
-- Resolve button
--------------------------------------------------------
 
if not resolved
and id ~= ''
then
 
renderResolveControl(
main,
id,
page,
false
)
 
end
 
 
--------------------------------------------------------
-- Attachments
--------------------------------------------------------
 
renderAttachments(
root,
entry
)
 
 
--------------------------------------------------------
-- Resolution
--------------------------------------------------------
 
if resolved then
 
renderResolution(
root,
entry
)
 
end
 
 
--------------------------------------------------------
-- Missing ID
--------------------------------------------------------
 
if not resolved
and id == ''
then
 
root:tag('div')
:addClass(
'feedback-missing-id'
)
:wikitext(
'Add an ID to enable the Resolve button.'
)
 
end
 
 
return tostring(root)
 
end
 
 
------------------------------------------------------------
-- Compact article card
------------------------------------------------------------
 
local function renderCompactCard(
entry
)
 
local resolved =
entry.status == 'resolved'
 
 
local id =
trim(entry.id)
 
 
local page =
trim(entry.page)
 
 
local root =
mw.html.create('div')
:addClass(
'feedback-page-card'
)
:addClass(
resolved
and 'feedback-page-card--resolved'
or 'feedback-page-card--unresolved'
)
 
 
if id ~= '' then
 
root:attr(
'data-feedback-id',
id
)
 
end
 
 
local content =
root:tag('div')
:addClass(
'feedback-page-card-content'
)
 
 
local firstLine =
content:tag('div')
:addClass(
'feedback-page-card-line'
)
 
 
firstLine:tag('span')
:addClass(
'feedback-status'
)
:addClass(
resolved
and 'feedback-status--resolved'
or 'feedback-status--unresolved'
)
:wikitext(
resolved
and 'RESOLVED'
or 'UNRESOLVED'
)
 
 
firstLine:tag('span')
:addClass(
'feedback-page-card-text'
)
:wikitext(
entry.feedback
or 'No feedback provided'
)
 
 
--------------------------------------------------------
-- Metadata
--------------------------------------------------------
 
local meta =
content:tag('div')
:addClass(
'feedback-page-card-meta'
)
 
 
if entry.user
and entry.user ~= ''
then
 
meta:wikitext(
formatUser(
entry.user
)
)
 
end
 
 
if entry.createddate
and entry.createddate ~= ''
then
 
meta:wikitext(
' · ' ..
mw.text.nowiki(
entry.createddate
)
)
 
end
 
 
--------------------------------------------------------
-- Resolution
--------------------------------------------------------
 
if resolved then
 
local resolvedBy =
trim(entry.resolvedby)
 
 
local resolvedDate =
trim(entry.resolveddate)
 
 
if resolvedBy ~= ''
or resolvedDate ~= ''
then
 
local resolution =
content:tag('div')
:addClass(
'feedback-page-card-resolution'
)
 
 
resolution:wikitext(
'↳ Resolved by '
)
 
 
if resolvedBy ~= '' then
 
resolution:wikitext(
formatUser(
resolvedBy
)
)
 
else
 
resolution:wikitext(
'Unknown'
)
 
end
 
 
if resolvedDate ~= '' then
 
resolution:wikitext(
' · ' ..
mw.text.nowiki(
resolvedDate
)
)
 
end
 
end
 
else
 
if id ~= '' then
 
renderResolveControl(
root,
id,
page,
true
)
 
end
 
end
 
 
--------------------------------------------------------
-- Attachments
--------------------------------------------------------
 
renderAttachments(
content,
entry
)
 
 
return tostring(root)
 
end
 
 
------------------------------------------------------------
-- Page feedback section
------------------------------------------------------------
 
function p.page(frame)
 
local page =
currentSubjectPage()
 
 
local allEntries =
queryFeedback(
page
)
 
 
if not allEntries then
return ''
end
 
 
if #allEntries == 0 then
return ''
end
 
 
local unresolved = {}
local resolved = {}
 
 
for _, row in ipairs(allEntries) do
 
if row.status == 'resolved' then
 
table.insert(
resolved,
row
)
 
else
 
table.insert(
unresolved,
row
)
 
end
 
end
 
 
local root =
mw.html.create('section')
:addClass(
'feedback-page-section'
)
:attr(
'id',
'feedback-section'
)
:attr(
'data-unresolved-count',
#unresolved
)
:attr(
'data-resolved-count',
#resolved
)
 
 
--------------------------------------------------------
-- Heading
--------------------------------------------------------
 
root:tag('h2')
:addClass(
'feedback-page-heading'
)
:wikitext(
'Feedbacks'
)
 
 
--------------------------------------------------------
-- Filters
--------------------------------------------------------
 
local filters =
root:tag('div')
:addClass(
'feedback-page-filters'
)
 
 
local unresolvedFilter =
filters:tag('span')
:addClass(
'feedback-page-filter'
)
:addClass(
'feedback-page-filter--active'
)
:attr(
'role',
'button'
)
:attr(
'tabindex',
'0'
)
:attr(
'data-feedback-filter',
'unresolved'
)
 
 
unresolvedFilter:wikitext(
'Unresolved '
)
 
 
unresolvedFilter:tag('span')
:addClass(
'feedback-page-filter-count'
)
:wikitext(
tostring(
#unresolved
)
)
 
 
if #resolved > 0 then
 
local resolvedFilter =
filters:tag('span')
:addClass(
'feedback-page-filter'
)
:attr(
'role',
'button'
)
:attr(
'tabindex',
'0'
)
:attr(
'data-feedback-filter',
'resolved'
)
 
 
resolvedFilter:wikitext(
'Resolved '
)
 
 
resolvedFilter:tag('span')
:addClass(
'feedback-page-filter-count'
)
:wikitext(
tostring(
#resolved
)
)
 
end
 
 
--------------------------------------------------------
-- Unresolved list
--------------------------------------------------------
 
local unresolvedList =
root:tag('div')
:addClass(
'feedback-page-list'
)
:addClass(
'feedback-page-list--unresolved'
)
 
 
if #unresolved > 0 then
 
for _, row in ipairs(unresolved) do
 
unresolvedList:node(
renderCompactCard(
row
)
)
 
end
 
else
 
unresolvedList:tag('div')
:addClass(
'feedback-page-empty'
)
:wikitext(
'No unresolved feedback.'
)
 
end
 
 
--------------------------------------------------------
-- Resolved list
--------------------------------------------------------
 
if #resolved > 0 then
 
local resolvedList =
root:tag('div')
:addClass(
'feedback-page-list'
)
:addClass(
'feedback-page-list--resolved'
)
 
 
for _, row in ipairs(resolved) do
 
resolvedList:node(
renderCompactCard(
row
)
)
 
end
 
end
 
 
return tostring(root)
 
end
 
 
------------------------------------------------------------
-- Feedback counts
------------------------------------------------------------
 
function p.counts(frame)
 
local page =
currentSubjectPage()
 
 
local entries =
queryFeedback(
page
)
 
 
if not entries then
return ''
end
 
 
local unresolved = 0
local resolved = 0
 
 
for _, row in ipairs(entries) do
 
if row.status == 'resolved' then
resolved = resolved + 1
else
unresolved = unresolved + 1
end


end
end
return tostring(
mw.html.create('span')
:addClass(
'feedback-count-data'
)
:attr(
'data-unresolved',
unresolved
)
:attr(
'data-resolved',
resolved
)
)
end
end
------------------------------------------------------------
-- Main / individual feedback
------------------------------------------------------------
function p.main(frame)
local parent =
frame:getParent()
local args =
parent
and parent.args
or frame.args
local feedback =
args.feedback
or 'No feedback provided'
local user =
args.user
or 'Anonymous'
local id =
trim(
args.id
)
local createdDate =
trim(
args.createddate
)
local link =
trim(
args.link
)
local image =
trim(
args.image
)
local resolvedBy =
trim(
args.resolvedby
)
local resolvedDate =
trim(
args.resolveddate
)
local resolved =
isTruthy(
args.resolved
)
local status =
resolved
and 'resolved'
or 'unresolved'
local page =
currentSubjectPage()
local saved, saveError =
saveFeedback({
page =
page,
user =
user,
status =
status,
feedback =
feedback,
id =
id,
createddate =
createdDate,
link =
link,
image =
image,
resolvedby =
resolvedBy,
resolveddate =
resolvedDate
})
if not saved then
return
'<strong style="color:red;">' ..
'Feedback bucket error: ' ..
mw.text.nowiki(
saveError or 'Unknown error'
) ..
'</strong>'
end
local card =
renderFullCard({
page =
page,
user =
user,
status =
status,
feedback =
feedback,
id =
id,
createddate =
createdDate,
link =
link,
image =
image,
resolvedby =
resolvedBy,
resolveddate =
resolvedDate
})
local categories = {}
if resolved then
table.insert(
categories,
'[[Category:Resolved Feedback]]'
)
else
table.insert(
categories,
'[[Category:Unresolved Feedback]]'
)
end
if id == '' then
table.insert(
categories,
'[[Category:Feedbacks Missing ID]]'
)
end
return card ..
table.concat(
categories
)
end
------------------------------------------------------------
-- Feedback table
------------------------------------------------------------
local function renderTable(
entries,
includeResolved
)
local tableRoot =
mw.html.create('table')
:addClass(
'feedback-table'
)
local header =
tableRoot:tag('tr')
if includeResolved then
header:tag('th')
:wikitext(
'Status'
)
end
header:tag('th')
:wikitext(
'Page'
)
header:tag('th')
:wikitext(
'Username'
)
header:tag('th')
:wikitext(
'Date'
)
header:tag('th')
:wikitext(
'Feedback'
)
if includeResolved then
header:tag('th')
:wikitext(
'Resolved by'
)
header:tag('th')
:wikitext(
'Resolved date'
)
end
if #entries == 0 then
local row =
tableRoot:tag('tr')
local colspan =
includeResolved
and '7'
or '4'
row:tag('td')
:attr(
'colspan',
colspan
)
:addClass(
'feedback-table-empty'
)
:wikitext(
'No feedback entries found.'
)
return tostring(
tableRoot
)
end
for _, entry in ipairs(entries) do
local row =
tableRoot:tag('tr')
:addClass(
entry.status == 'resolved'
and 'feedback-table-row--resolved'
or 'feedback-table-row--unresolved'
)
if includeResolved then
row:tag('td')
:addClass(
'feedback-table-status'
)
:wikitext(
entry.status == 'resolved'
and 'Resolved'
or 'Unresolved'
)
end
row:tag('td')
:addClass(
'feedback-table-page'
)
:wikitext(
'[[' ..
entry.page ..
']]'
)
row:tag('td')
:addClass(
'feedback-table-user'
)
:wikitext(
formatUser(
entry.user
)
)
row:tag('td')
:addClass(
'feedback-table-date'
)
:wikitext(
mw.text.nowiki(
entry.createddate
or '—'
)
)
row:tag('td')
:addClass(
'feedback-table-feedback'
)
:wikitext(
entry.feedback
or ''
)
if includeResolved then
row:tag('td')
:addClass(
'feedback-table-resolved-by'
)
:wikitext(
entry.resolvedby
and formatUser(
entry.resolvedby
)
or '—'
)
row:tag('td')
:addClass(
'feedback-table-resolved-date'
)
:wikitext(
entry.resolveddate
and mw.text.nowiki(
entry.resolveddate
)
or '—'
)
end
end
return tostring(
tableRoot
)
end
------------------------------------------------------------
-- Global feedback page
------------------------------------------------------------
function p.view(frame)
local parent =
frame:getParent()
local args =
parent
and parent.args
or frame.args
local status =
trim(
args.status or ''
):lower()
--------------------------------------------------------
-- Specific table
--------------------------------------------------------
if status == 'resolved'
or status == 'unresolved'
then
local entries =
queryFeedback(
nil,
status
)
if not entries then
return ''
end
return renderTable(
entries,
false
)
end
--------------------------------------------------------
-- Dashboard
--------------------------------------------------------
local unresolved =
queryFeedback(
nil,
'unresolved'
)
local all =
queryFeedback(
nil,
'all'
)
if not unresolved then
unresolved = {}
end
if not all then
all = {}
end
local root =
mw.html.create('div')
:addClass(
'feedback-dashboard'
)
root:tag('h2')
:addClass(
'feedback-dashboard-heading'
)
:wikitext(
'Unresolved Feedback'
)
root:node(
renderTable(
unresolved,
false
)
)
root:tag('h2')
:addClass(
'feedback-dashboard-heading'
)
:wikitext(
'All Feedback'
)
root:node(
renderTable(
all,
true
)
)
return tostring(
root
)
end
------------------------------------------------------------
-- Aliases
------------------------------------------------------------
p.table = p.view
return p

Revision as of 11:52, 16 September 2026

Module documentation

This module implements {{Feedback}}.

local p = {}


------------------------------------------------------------
-- Helpers
------------------------------------------------------------

local function isTruthy(value)

	if value == nil then
		return false
	end

	value =
		mw.text.trim(
			tostring(value)
		):lower()

	return value ~= ''
		and value ~= '0'
		and value ~= 'false'
		and value ~= 'no'
		and value ~= 'off'
end


local function trim(value)

	return mw.text.trim(
		tostring(value or '')
	)

end


local function formatUser(user)

	user =
		trim(user)

	if user == ''
		or user:lower() == 'anonymous'
	then
		return 'Anonymous'
	end

	return string.format(
		'[[User:%s|%s]]',
		user,
		user
	)

end


local function currentSubjectPage()

	local title =
		mw.title.getCurrentTitle()

	return title.subjectPageTitle.fullText

end


local function getBucket()

	if not (mw.ext and mw.ext.bucket) then
		return nil, 'Bucket extension is not available'
	end


	local bucket =
		mw.ext.bucket('feedback')


	if not bucket then
		return nil, 'Feedback bucket could not be opened'
	end


	return bucket

end


local function sortNewestFirst(results)

	if not results then
		return {}
	end


	table.sort(
		results,
		function(a, b)

			local aDate =
				trim(a.createddate)

			local bDate =
				trim(b.createddate)


			if aDate == ''
				and bDate ~= ''
			then
				return false
			end


			if aDate ~= ''
				and bDate == ''
			then
				return true
			end


			if aDate ~= bDate then
				return aDate > bDate
			end


			return trim(a.id) > trim(b.id)

		end
	)


	return results

end


local function queryFeedback(
	page,
	status
)

	local bucket, err =
		getBucket()


	if not bucket then
		return nil, err
	end


	local query =
		bucket.select(
			'page',
			'user',
			'status',
			'feedback',
			'id',
			'createddate',
			'link',
			'image',
			'resolvedby',
			'resolveddate'
		)


	if page and page ~= '' then

		query.where(
			'page',
			page
		)

	end


	if status
		and status ~= ''
		and status ~= 'all'
	then

		query.where(
			'status',
			status
		)

	end


	local success, results =
		pcall(
			function()
				return query.run()
			end
		)


	if not success then
		return nil, tostring(results)
	end


	return sortNewestFirst(
		results or {}
	)

end


------------------------------------------------------------
-- Save feedback
------------------------------------------------------------

local function saveFeedback(data)

	local bucket, err =
		getBucket()


	if not bucket then
		return false, err
	end


	local success, saveError =
		pcall(
			function()

				bucket.put({

					page =
						data.page,

					user =
						data.user,

					status =
						data.status,

					feedback =
						data.feedback,

					id =
						data.id,

					createddate =
						data.createddate,

					link =
						data.link,

					image =
						data.image,

					resolvedby =
						data.resolvedby,

					resolveddate =
						data.resolveddate

				})

			end
		)


	if not success then
		return false, tostring(saveError)
	end


	return true

end


------------------------------------------------------------
-- Shared metadata
------------------------------------------------------------

local function renderMetadata(
	root,
	entry
)

	local meta =
		root:tag('div')
			:addClass(
				'feedback-meta'
			)


	if entry.user
		and entry.user ~= ''
	then

		meta:tag('span')
			:addClass(
				'feedback-user'
			)
			:wikitext(
				formatUser(
					entry.user
				)
			)

	end


	if entry.createddate
		and entry.createddate ~= ''
	then

		meta:tag('span')
			:addClass(
				'feedback-meta-date'
			)
			:wikitext(
				' · ' ..
				mw.text.nowiki(
					entry.createddate
				)
			)

	end

end


------------------------------------------------------------
-- Attachments
------------------------------------------------------------

local function renderAttachments(
	root,
	entry
)

	local link =
		trim(entry.link)


	local image =
		trim(entry.image)


	if link == ''
		and image == ''
	then
		return
	end


	local attachments =
		root:tag('div')
			:addClass(
				'feedback-attachments'
			)


	if link ~= '' then

		attachments:tag('span')
			:addClass(
				'feedback-attachment'
			)
			:wikitext(
				'🔗 ' ..
				'[' ..
				link ..
				' Link]'
			)

	end


	if image ~= '' then

		local imageAttachment =
			attachments:tag('span')
				:addClass(
					'feedback-attachment'
				)


		imageAttachment:tag('span')
			:addClass(
				'feedback-image-icon'
			)
			:wikitext('▣')


		imageAttachment:tag('span')
			:addClass(
				'feedback-image-link'
			)
			:wikitext(
				'[[Media:' ..
				image ..
				'|Picture]]'
			)

	end

end


------------------------------------------------------------
-- Resolution information
------------------------------------------------------------

local function renderResolution(
	root,
	entry
)

	local resolvedBy =
		trim(entry.resolvedby)


	local resolvedDate =
		trim(entry.resolveddate)


	if resolvedBy == ''
		and resolvedDate == ''
	then
		return
	end


	local resolution =
		root:tag('div')
			:addClass(
				'feedback-resolution'
			)


	resolution:wikitext(
		'↳ Resolved by '
	)


	if resolvedBy ~= '' then

		resolution:wikitext(
			formatUser(
				resolvedBy
			)
		)

	else

		resolution:wikitext(
			'Unknown'
		)

	end


	if resolvedDate ~= '' then

		resolution:wikitext(
			' on ' ..
			mw.text.nowiki(
				resolvedDate
			)
		)

	end

end


------------------------------------------------------------
-- Resolve control
------------------------------------------------------------

local function renderResolveControl(
	parent,
	id,
	page,
	compact
)

	if not id
		or id == ''
	then
		return
	end


	local control =
		parent:tag('span')
			:addClass(
				'feedback-resolve-button'
			)



	control:attr(
		'data-feedback-id',
		id
	)


	if page
		and page ~= ''
	then

		control:attr(
			'data-feedback-page',
			page
		)

	end


	if compact then

		control
			:addClass(
				'feedback-resolve-button--compact'
			)
			:attr(
				'role',
				'button'
			)
			:attr(
				'tabindex',
				'0'
			)
			:attr(
				'aria-label',
				'Resolve feedback'
			)
			:attr(
				'title',
				'Resolve feedback'
			)
			:wikitext(
				'✓'
			)

	else

		control
			:attr(
				'role',
				'button'
			)
			:attr(
				'tabindex',
				'0'
			)
			:attr(
				'aria-label',
				'Resolve feedback'
			)
			:wikitext(
				'Resolve'
			)

	end

end


------------------------------------------------------------
-- Full feedback card
------------------------------------------------------------

local function renderFullCard(
	entry
)

	local resolved =
		entry.status == 'resolved'


	local id =
		trim(entry.id)


	local page =
		trim(entry.page)


	local root =
		mw.html.create('div')
			:addClass(
				'feedback-box'
			)
			:addClass(
				resolved
					and 'feedback-box--resolved'
					or 'feedback-box--unresolved'
			)


	if id ~= '' then

		root:attr(
			'data-feedback-id',
			id
		)

	end


	--------------------------------------------------------
	-- Main line
	--------------------------------------------------------

	local main =
		root:tag('div')
			:addClass(
				'feedback-main-line'
			)


	main:tag('span')
		:addClass(
			'feedback-status'
		)
		:addClass(
			resolved
				and 'feedback-status--resolved'
				or 'feedback-status--unresolved'
		)
		:wikitext(
			resolved
				and 'RESOLVED'
				or 'UNRESOLVED'
		)


	main:tag('span')
		:addClass(
			'feedback-text'
		)
		:wikitext(
			entry.feedback
				or 'No feedback provided'
		)


	--------------------------------------------------------
	-- Inline metadata
	--------------------------------------------------------

	local metadata =
		main:tag('span')
			:addClass(
				'feedback-inline-meta'
			)


	if entry.user
		and entry.user ~= ''
	then

		metadata:wikitext(
			' · ' ..
			formatUser(
				entry.user
			)
		)

	end


	if entry.createddate
		and entry.createddate ~= ''
	then

		metadata:wikitext(
			' ' ..
			mw.text.nowiki(
				entry.createddate
			)
		)

	end


	--------------------------------------------------------
	-- Resolve button
	--------------------------------------------------------

	if not resolved
		and id ~= ''
	then

		renderResolveControl(
			main,
			id,
			page,
			false
		)

	end


	--------------------------------------------------------
	-- Attachments
	--------------------------------------------------------

	renderAttachments(
		root,
		entry
	)


	--------------------------------------------------------
	-- Resolution
	--------------------------------------------------------

	if resolved then

		renderResolution(
			root,
			entry
		)

	end


	--------------------------------------------------------
	-- Missing ID
	--------------------------------------------------------

	if not resolved
		and id == ''
	then

		root:tag('div')
			:addClass(
				'feedback-missing-id'
			)
			:wikitext(
				'Add an ID to enable the Resolve button.'
			)

	end


	return tostring(root)

end


------------------------------------------------------------
-- Compact article card
------------------------------------------------------------

local function renderCompactCard(
	entry
)

	local resolved =
		entry.status == 'resolved'


	local id =
		trim(entry.id)


	local page =
		trim(entry.page)


	local root =
		mw.html.create('div')
			:addClass(
				'feedback-page-card'
			)
			:addClass(
				resolved
					and 'feedback-page-card--resolved'
					or 'feedback-page-card--unresolved'
			)


	if id ~= '' then

		root:attr(
			'data-feedback-id',
			id
		)

	end


	local content =
		root:tag('div')
			:addClass(
				'feedback-page-card-content'
			)


	local firstLine =
		content:tag('div')
			:addClass(
				'feedback-page-card-line'
			)


	firstLine:tag('span')
		:addClass(
			'feedback-status'
		)
		:addClass(
			resolved
				and 'feedback-status--resolved'
				or 'feedback-status--unresolved'
		)
		:wikitext(
			resolved
				and 'RESOLVED'
				or 'UNRESOLVED'
		)


	firstLine:tag('span')
		:addClass(
			'feedback-page-card-text'
		)
		:wikitext(
			entry.feedback
				or 'No feedback provided'
		)


	--------------------------------------------------------
	-- Metadata
	--------------------------------------------------------

	local meta =
		content:tag('div')
			:addClass(
				'feedback-page-card-meta'
			)


	if entry.user
		and entry.user ~= ''
	then

		meta:wikitext(
			formatUser(
				entry.user
			)
		)

	end


	if entry.createddate
		and entry.createddate ~= ''
	then

		meta:wikitext(
			' · ' ..
			mw.text.nowiki(
				entry.createddate
			)
		)

	end


	--------------------------------------------------------
	-- Resolution
	--------------------------------------------------------

	if resolved then

		local resolvedBy =
			trim(entry.resolvedby)


		local resolvedDate =
			trim(entry.resolveddate)


		if resolvedBy ~= ''
			or resolvedDate ~= ''
		then

			local resolution =
				content:tag('div')
					:addClass(
						'feedback-page-card-resolution'
					)


			resolution:wikitext(
				'↳ Resolved by '
			)


			if resolvedBy ~= '' then

				resolution:wikitext(
					formatUser(
						resolvedBy
					)
				)

			else

				resolution:wikitext(
					'Unknown'
				)

			end


			if resolvedDate ~= '' then

				resolution:wikitext(
					' · ' ..
					mw.text.nowiki(
						resolvedDate
					)
				)

			end

		end

	else

		if id ~= '' then

			renderResolveControl(
				root,
				id,
				page,
				true
			)

		end

	end


	--------------------------------------------------------
	-- Attachments
	--------------------------------------------------------

	renderAttachments(
		content,
		entry
	)


	return tostring(root)

end


------------------------------------------------------------
-- Page feedback section
------------------------------------------------------------

function p.page(frame)

	local page =
		currentSubjectPage()


	local allEntries =
		queryFeedback(
			page
		)


	if not allEntries then
		return ''
	end


	if #allEntries == 0 then
		return ''
	end


	local unresolved = {}
	local resolved = {}


	for _, row in ipairs(allEntries) do

		if row.status == 'resolved' then

			table.insert(
				resolved,
				row
			)

		else

			table.insert(
				unresolved,
				row
			)

		end

	end


	local root =
		mw.html.create('section')
			:addClass(
				'feedback-page-section'
			)
			:attr(
				'id',
				'feedback-section'
			)
			:attr(
				'data-unresolved-count',
				#unresolved
			)
			:attr(
				'data-resolved-count',
				#resolved
			)


	--------------------------------------------------------
	-- Heading
	--------------------------------------------------------

	root:tag('h2')
		:addClass(
			'feedback-page-heading'
		)
		:wikitext(
			'Feedbacks'
		)


	--------------------------------------------------------
	-- Filters
	--------------------------------------------------------

	local filters =
		root:tag('div')
			:addClass(
				'feedback-page-filters'
			)


	local unresolvedFilter =
		filters:tag('span')
			:addClass(
				'feedback-page-filter'
			)
			:addClass(
				'feedback-page-filter--active'
			)
			:attr(
				'role',
				'button'
			)
			:attr(
				'tabindex',
				'0'
			)
			:attr(
				'data-feedback-filter',
				'unresolved'
			)


	unresolvedFilter:wikitext(
		'Unresolved '
	)


	unresolvedFilter:tag('span')
		:addClass(
			'feedback-page-filter-count'
		)
		:wikitext(
			tostring(
				#unresolved
			)
		)


	if #resolved > 0 then

		local resolvedFilter =
			filters:tag('span')
				:addClass(
					'feedback-page-filter'
				)
				:attr(
					'role',
					'button'
				)
				:attr(
					'tabindex',
					'0'
				)
				:attr(
					'data-feedback-filter',
					'resolved'
				)


		resolvedFilter:wikitext(
			'Resolved '
		)


		resolvedFilter:tag('span')
			:addClass(
				'feedback-page-filter-count'
			)
			:wikitext(
				tostring(
					#resolved
				)
			)

	end


	--------------------------------------------------------
	-- Unresolved list
	--------------------------------------------------------

	local unresolvedList =
		root:tag('div')
			:addClass(
				'feedback-page-list'
			)
			:addClass(
				'feedback-page-list--unresolved'
			)


	if #unresolved > 0 then

		for _, row in ipairs(unresolved) do

			unresolvedList:node(
				renderCompactCard(
					row
				)
			)

		end

	else

		unresolvedList:tag('div')
			:addClass(
				'feedback-page-empty'
			)
			:wikitext(
				'No unresolved feedback.'
			)

	end


	--------------------------------------------------------
	-- Resolved list
	--------------------------------------------------------

	if #resolved > 0 then

		local resolvedList =
			root:tag('div')
				:addClass(
					'feedback-page-list'
				)
				:addClass(
					'feedback-page-list--resolved'
				)


		for _, row in ipairs(resolved) do

			resolvedList:node(
				renderCompactCard(
					row
				)
			)

		end

	end


	return tostring(root)

end


------------------------------------------------------------
-- Feedback counts
------------------------------------------------------------

function p.counts(frame)

	local page =
		currentSubjectPage()


	local entries =
		queryFeedback(
			page
		)


	if not entries then
		return ''
	end


	local unresolved = 0
	local resolved = 0


	for _, row in ipairs(entries) do

		if row.status == 'resolved' then
			resolved = resolved + 1
		else
			unresolved = unresolved + 1
		end

	end


	return tostring(

		mw.html.create('span')
			:addClass(
				'feedback-count-data'
			)
			:attr(
				'data-unresolved',
				unresolved
			)
			:attr(
				'data-resolved',
				resolved
			)

	)

end


------------------------------------------------------------
-- Main / individual feedback
------------------------------------------------------------

function p.main(frame)

	local parent =
		frame:getParent()


	local args =
		parent
			and parent.args
			or frame.args


	local feedback =
		args.feedback
			or 'No feedback provided'


	local user =
		args.user
			or 'Anonymous'


	local id =
		trim(
			args.id
		)


	local createdDate =
		trim(
			args.createddate
		)


	local link =
		trim(
			args.link
		)


	local image =
		trim(
			args.image
		)


	local resolvedBy =
		trim(
			args.resolvedby
		)


	local resolvedDate =
		trim(
			args.resolveddate
		)


	local resolved =
		isTruthy(
			args.resolved
		)


	local status =
		resolved
			and 'resolved'
			or 'unresolved'


	local page =
		currentSubjectPage()


	local saved, saveError =
		saveFeedback({

			page =
				page,

			user =
				user,

			status =
				status,

			feedback =
				feedback,

			id =
				id,

			createddate =
				createdDate,

			link =
				link,

			image =
				image,

			resolvedby =
				resolvedBy,

			resolveddate =
				resolvedDate

		})


	if not saved then

		return
			'<strong style="color:red;">' ..
			'Feedback bucket error: ' ..
			mw.text.nowiki(
				saveError or 'Unknown error'
			) ..
			'</strong>'

	end


	local card =
		renderFullCard({

			page =
				page,

			user =
				user,

			status =
				status,

			feedback =
				feedback,

			id =
				id,

			createddate =
				createdDate,

			link =
				link,

			image =
				image,

			resolvedby =
				resolvedBy,

			resolveddate =
				resolvedDate

		})


	local categories = {}


	if resolved then

		table.insert(
			categories,
			'[[Category:Resolved Feedback]]'
		)

	else

		table.insert(
			categories,
			'[[Category:Unresolved Feedback]]'
		)

	end


	if id == '' then

		table.insert(
			categories,
			'[[Category:Feedbacks Missing ID]]'
		)

	end


	return card ..
		table.concat(
			categories
		)

end


------------------------------------------------------------
-- Feedback table
------------------------------------------------------------

local function renderTable(
	entries,
	includeResolved
)

	local tableRoot =
		mw.html.create('table')
			:addClass(
				'feedback-table'
			)


	local header =
		tableRoot:tag('tr')


	if includeResolved then

		header:tag('th')
			:wikitext(
				'Status'
			)

	end


	header:tag('th')
		:wikitext(
			'Page'
		)


	header:tag('th')
		:wikitext(
			'Username'
		)


	header:tag('th')
		:wikitext(
			'Date'
		)


	header:tag('th')
		:wikitext(
			'Feedback'
		)


	if includeResolved then

		header:tag('th')
			:wikitext(
				'Resolved by'
			)


		header:tag('th')
			:wikitext(
				'Resolved date'
			)

	end


	if #entries == 0 then

		local row =
			tableRoot:tag('tr')


		local colspan =
			includeResolved
				and '7'
				or '4'


		row:tag('td')
			:attr(
				'colspan',
				colspan
			)
			:addClass(
				'feedback-table-empty'
			)
			:wikitext(
				'No feedback entries found.'
			)


		return tostring(
			tableRoot
		)

	end


	for _, entry in ipairs(entries) do

		local row =
			tableRoot:tag('tr')
				:addClass(
					entry.status == 'resolved'
						and 'feedback-table-row--resolved'
						or 'feedback-table-row--unresolved'
				)


		if includeResolved then

			row:tag('td')
				:addClass(
					'feedback-table-status'
				)
				:wikitext(
					entry.status == 'resolved'
						and 'Resolved'
						or 'Unresolved'
				)

		end


		row:tag('td')
			:addClass(
				'feedback-table-page'
			)
			:wikitext(
				'[[' ..
				entry.page ..
				']]'
			)


		row:tag('td')
			:addClass(
				'feedback-table-user'
			)
			:wikitext(
				formatUser(
					entry.user
				)
			)


		row:tag('td')
			:addClass(
				'feedback-table-date'
			)
			:wikitext(
				mw.text.nowiki(
					entry.createddate
						or '—'
				)
			)


		row:tag('td')
			:addClass(
				'feedback-table-feedback'
			)
			:wikitext(
				entry.feedback
					or ''
			)


		if includeResolved then

			row:tag('td')
				:addClass(
					'feedback-table-resolved-by'
				)
				:wikitext(
					entry.resolvedby
						and formatUser(
							entry.resolvedby
						)
						or '—'
				)


			row:tag('td')
				:addClass(
					'feedback-table-resolved-date'
				)
				:wikitext(
					entry.resolveddate
						and mw.text.nowiki(
							entry.resolveddate
						)
						or '—'
				)

		end

	end


	return tostring(
		tableRoot
	)

end


------------------------------------------------------------
-- Global feedback page
------------------------------------------------------------

function p.view(frame)

	local parent =
		frame:getParent()


	local args =
		parent
			and parent.args
			or frame.args


	local status =
		trim(
			args.status or ''
		):lower()


	--------------------------------------------------------
	-- Specific table
	--------------------------------------------------------

	if status == 'resolved'
		or status == 'unresolved'
	then

		local entries =
			queryFeedback(
				nil,
				status
			)


		if not entries then
			return ''
		end


		return renderTable(
			entries,
			false
		)

	end


	--------------------------------------------------------
	-- Dashboard
	--------------------------------------------------------

	local unresolved =
		queryFeedback(
			nil,
			'unresolved'
		)


	local all =
		queryFeedback(
			nil,
			'all'
		)


	if not unresolved then
		unresolved = {}
	end


	if not all then
		all = {}
	end


	local root =
		mw.html.create('div')
			:addClass(
				'feedback-dashboard'
			)


	root:tag('h2')
		:addClass(
			'feedback-dashboard-heading'
		)
		:wikitext(
			'Unresolved Feedback'
		)


	root:node(
		renderTable(
			unresolved,
			false
		)
	)


	root:tag('h2')
		:addClass(
			'feedback-dashboard-heading'
		)
		:wikitext(
			'All Feedback'
		)


	root:node(
		renderTable(
			all,
			true
		)
	)


	return tostring(
		root
	)

end


------------------------------------------------------------
-- Aliases
------------------------------------------------------------

p.table = p.view


return p