Jump to content

Module:Feedback: Difference between revisions

From The Petit Planet Wiki
ReisuDesign (talk | contribs)
No edit summary
ReisuDesign (talk | contribs)
No edit summary
 
(18 intermediate revisions by the same user not shown)
Line 2: Line 2:


------------------------------------------------------------
------------------------------------------------------------
-- Helpers
-- HELPERS
------------------------------------------------------------
------------------------------------------------------------


local function isTruthy(v)
local function isTruthy(value)
if v == nil then
if value == nil then
return false
return false
end
end


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


return v ~= ''
return value == 'true'
and v ~= '0'
or value == 'yes'
and v ~= 'false'
or value == '1'
and v ~= 'no'
or value == 'resolved'
and v ~= 'off'
end
 
 
local function trim(value)
if value == nil then
return ''
end
 
return mw.text.trim(tostring(value))
end
end




local function formatUser(user)
local function formatUser(user)
user = mw.text.trim(tostring(user or ''))
user = trim(user)


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




local function escapeAttribute(value)
local function currentSubjectPage()
value = tostring(value or '')
local title = mw.title.getCurrentTitle()
 
if title.subjectPageTitle then
return title.subjectPageTitle.fullText
end
 
return title.fullText
end
 
 
local function getBucket()
if not (mw.ext and mw.ext.bucket) then
return nil
end
 
return mw.ext.bucket('feedback')
end
 
 
local function sortNewestFirst(entries)
table.sort(
entries,
function(a, b)
local aDate = trim(a.createddate)
local bDate = trim(b.createddate)
 
return aDate > bDate
end
)
 
return entries
end
 
 
local function queryFeedback(page)
local bucket = getBucket()
 
if not bucket then
return {}
end
 
local query = bucket
 
query.select(
'page',
'user',
'status',
'feedback',
'id',
'createddate',
'link',
'image',
'resolvedby',
'resolveddate'
)
 
query.where(
'page',
page
)
 
local success, results = pcall(function()
return query.run()
end)
 
if not success or not results then
return {}
end
 
return sortNewestFirst(results)
end
 
 
------------------------------------------------------------
-- ATTACHMENTS
------------------------------------------------------------
 
local function renderAttachments(entry)
local wrapper =
mw.html.create('div')
:addClass('feedback-attachments')
 
local link = trim(entry.link)
 
if link ~= '' then
local linkWrapper =
wrapper:tag('span')
:addClass('feedback-attachment-link')
linkWrapper:tag('span')
:addClass('feedback-attachment-icon')
linkWrapper:wikitext(
'[' .. link .. ' ' .. mw.text.nowiki(link) .. ']'
)
end
 
local image = trim(entry.image)
 
if image ~= '' then
image = mw.ustring.gsub(
image,
'^File:',
''
)
 
local title =
mw.title.new('File:' .. image)
 
if title then
local imageWrapper =
wrapper:tag('span')
:addClass('feedback-attachment-image')
imageWrapper:tag('span')
:addClass('feedback-attachment-icon')
imageWrapper:wikitext(
'[' ..
title:fullUrl() ..
' ' ..
mw.text.nowiki(image) ..
']'
)
end
end


return mw.text.nowiki(value)
return wrapper
end
end




------------------------------------------------------------
------------------------------------------------------------
-- Feedback card
-- RESOLUTION
------------------------------------------------------------
------------------------------------------------------------


local function renderCard(entry, showInstruction)
local function renderResolution(parent, entry)
local resolvedBy = trim(entry.resolvedby)
local resolvedDate = trim(entry.resolveddate)
 
if resolvedBy == '' and resolvedDate == '' then
return
end


local resolved = (entry.status == 'resolved')
local resolution =
parent:tag('div')
:addClass('feedback-resolution')


local user = entry.user or 'Anonymous'
resolution:wikitext('Resolved by ')
local feedback = entry.feedback or 'No feedback provided'
local page = entry.page
local id = entry.id
local resolvedBy = entry.resolvedby
local resolvedDate = entry.resolveddate


if resolvedBy ~= '' then
resolution:wikitext(
formatUser(resolvedBy)
)
else
resolution:wikitext('Unknown')
end


local root = mw.html.create('div')
if resolvedDate ~= '' then
:addClass('feedback-box')
resolution:wikitext(
:addClass(
' on ' .. mw.text.nowiki(resolvedDate)
resolved
and 'feedback-box--resolved'
or 'feedback-box--unresolved'
)
)
end
end




--------------------------------------------------------
------------------------------------------------------------
-- Metadata
-- RESOLVE CONTROL
--------------------------------------------------------
------------------------------------------------------------


local meta = root:tag('div')
local function renderResolveControl(
:addClass('feedback-meta')
parent,
id,
page,
compact
)
if trim(id) == '' then
return
end


local wrapper =
parent:tag('div')
:addClass('feedback-resolve')


if page and page ~= '' then
local control =
wrapper:tag('span')
:addClass('feedback-resolve-button')
:attr(
'data-feedback-id',
id
)
:attr(
'data-feedback-page',
page or ''
)
:attr(
'role',
'button'
)
:attr(
'tabindex',
'0'
)
:attr(
'aria-label',
'Resolve feedback'
)


meta:tag('span')
if compact then
:addClass('feedback-page')
control
:wikitext(
:addClass(
string.format('[[%s]]', page)
'feedback-resolve-button--compact'
)
:attr(
'title',
'Resolve feedback'
)
)
:wikitext('✓')
else
control:wikitext('Resolve')
end
end
------------------------------------------------------------
-- FULL FEEDBACK CARD
------------------------------------------------------------
local function renderFullCard(entry)
local resolved =
trim(entry.status):lower() == 'resolved'


meta:tag('span')
local id = trim(entry.id)
:addClass('feedback-separator')
local page = trim(entry.page)
:wikitext(' • ')
local feedback = trim(entry.feedback)
local user = trim(entry.user)
local createddate = trim(entry.createddate)


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
end


--------------------------------------------------------
-- Main content column
--------------------------------------------------------


meta:tag('span')
local content =
:addClass('feedback-user')
root:tag('div')
:wikitext(formatUser(user))
:addClass('feedback-content')


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


meta:tag('span')
local main =
:addClass('feedback-separator')
content:tag('div')
:wikitext(' • ')
:addClass('feedback-main-line')


--------------------------------------------------------
-- Status
--------------------------------------------------------


meta:tag('span')
main:tag('span')
:addClass('feedback-status')
:addClass('feedback-status')
:addClass(
:addClass(
Line 112: Line 329:
or 'Unresolved'
or 'Unresolved'
)
)


--------------------------------------------------------
--------------------------------------------------------
Line 118: Line 334:
--------------------------------------------------------
--------------------------------------------------------


root:tag('div')
main:tag('span')
:addClass('feedback-body')
:addClass('feedback-text')
:wikitext(feedback)
:wikitext(
 
feedback ~= ''
and feedback
or 'No feedback provided'
)


--------------------------------------------------------
--------------------------------------------------------
-- Resolution information
-- Metadata
--------------------------------------------------------
--------------------------------------------------------


if resolved and resolvedBy and resolvedBy ~= '' then
local meta =
main:tag('span')
:addClass('feedback-inline-meta')


local resolutionMeta =
if user ~= '' then
'Resolved by ' .. formatUser(resolvedBy)
meta:wikitext(
 
' · ' .. formatUser(user)
if resolvedDate and resolvedDate ~= '' then
)
resolutionMeta =
end
resolutionMeta
.. ' on '
.. resolvedDate
end
 
root:tag('div')
:addClass('feedback-resolution')
:wikitext(resolutionMeta)


if createddate ~= '' then
meta:wikitext(
' · ' .. mw.text.nowiki(createddate)
)
end
end


--------------------------------------------------------
-- Attachments
--------------------------------------------------------
content:node(
    renderAttachments(entry)
)


--------------------------------------------------------
--------------------------------------------------------
-- Resolution controls
-- Resolution information
--------------------------------------------------------
--------------------------------------------------------


if showInstruction and not resolved then
renderResolution(
 
content,
local instruction = root:tag('div')
entry
:addClass('feedback-instruction')
)


--------------------------------------------------------
-- Missing ID warning
--------------------------------------------------------


instruction:tag('span')
if id == '' then
content:tag('div')
:addClass('feedback-missing-id')
:wikitext(
:wikitext(
'If this feedback has been resolved, '
'This feedback entry is missing an ID.'
.. 'click the button below.'
)
)
end


--------------------------------------------------------
-- Resolve button
--
-- IMPORTANT:
-- This is outside .feedback-content so it
-- remains on the right.
--------------------------------------------------------


if id and id ~= '' then
if not resolved then
 
renderResolveControl(
instruction:tag('button')
root,
:addClass('feedback-resolve-button')
id,
:attr('type', 'button')
page,
:attr('data-feedback-id', escapeAttribute(id))
false
:wikitext('Resolve')
)
 
end
 
end
end


return tostring(root)
return tostring(root)
Line 181: Line 413:


------------------------------------------------------------
------------------------------------------------------------
-- Save feedback
-- COMPACT CARD
------------------------------------------------------------
------------------------------------------------------------


local function saveFeedback(data)
local function renderCompactCard(entry)
return renderFullCard(entry)
end


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


------------------------------------------------------------
-- SAVE FEEDBACK
------------------------------------------------------------


local bucket = mw.ext.bucket('feedback')
local function saveFeedback(data)
local bucket = getBucket()


if not bucket then
if not bucket then
return false, 'Feedback bucket could not be opened'
return false, 'Bucket extension is not available'
end
end


local success, err = pcall(function()
local success, err = pcall(function()
bucket.put({
bucket.put({
page = data.page,
page = data.page,
user = data.user,
user = data.user,
status = data.status,
status = data.status,
feedback = data.feedback
feedback = data.feedback,
id = data.id,
createddate = data.createddate,
link = data.link,
image = data.image,
resolvedby = data.resolvedby,
resolveddate = data.resolveddate
})
})
end)
end)


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


return true
return true
Line 220: Line 456:


------------------------------------------------------------
------------------------------------------------------------
-- Main
-- MAIN
--
--
-- Used by Template:Feedback
-- Used by Template:Feedback
--
-- Example:
--
-- {{Feedback
--  |feedback=Something could be improved here.
--  |user=Rei
--  |resolved=false
--  |id=1758012345678
--  |resolvedby=
--  |resolveddate=
-- }}
------------------------------------------------------------
------------------------------------------------------------


function p.main(frame)
function p.main(frame)
local parent = frame:getParent()
local parent = frame:getParent()


Line 243: Line 467:
parent and parent.args
parent and parent.args
or frame.args
or frame.args
--------------------------------------------------------
-- Get page title
--------------------------------------------------------


local pageTitle = nil
local pageTitle = nil


if parent and parent.getTitle then
if parent and parent.getTitle then
local title = parent:getTitle()
local title = parent:getTitle()


Line 261: Line 479:
pageTitle = title
pageTitle = title
end
end
end
end


 
return p._main(
return p._main(args, pageTitle)
args,
 
pageTitle
)
end
end




function p._main(args, pageTitle)
function p._main(args, pageTitle)
local feedback =
local feedback =
args.feedback
trim(args.feedback)
or 'No feedback provided'


if feedback == '' then
feedback = 'No feedback provided'
end


local user =
local user =
args.user
trim(args.user)
or 'Anonymous'


if user == '' then
user = 'Anonymous'
end


local resolved =
local resolved =
isTruthy(args.resolved)
isTruthy(args.resolved)


local status =
local status =
Line 291: Line 511:
or 'unresolved'
or 'unresolved'


local currentPage =
trim(pageTitle)
if currentPage == '' then
currentPage =
mw.title.getCurrentTitle().fullText
end


local id =
local id =
mw.text.trim(
trim(args.id)
tostring(args.id or '')
)
 
 
local resolvedBy =
mw.text.trim(
tostring(args.resolvedby or '')
)


local createddate =
trim(args.createddate)


local resolvedDate =
local link =
mw.text.trim(
trim(args.link)
tostring(args.resolveddate or '')
)


local image =
trim(args.image)


local currentPage =
local resolvedby =
pageTitle
trim(args.resolvedby)
or mw.title.getCurrentTitle().fullText


local resolveddate =
trim(args.resolveddate)


--------------------------------------------------------
--------------------------------------------------------
Line 319: Line 541:
--------------------------------------------------------
--------------------------------------------------------


local saved, saveError = saveFeedback({
local saved, saveError =
page = currentPage,
saveFeedback({
user = user,
page = currentPage,
status = status,
user = user,
feedback = feedback
status = status,
})
feedback = feedback,
 
id = id,
createddate = createddate,
link = link,
image = image,
resolvedby = resolvedby,
resolveddate = resolveddate
})


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


--------------------------------------------------------
--------------------------------------------------------
-- Render feedback card
-- Render
--------------------------------------------------------
--------------------------------------------------------


local card = renderCard({
local card =
 
renderFullCard({
page = nil,
page = currentPage,
 
user = user,
user = user,
status = status,
 
feedback = feedback,
status = status,
id = id,
 
createddate = createddate,
feedback = feedback,
link = link,
 
image = image,
id = id,
resolvedby = resolvedby,
 
resolveddate = resolveddate
resolvedby = resolvedBy,
})
 
resolveddate = resolvedDate
 
}, true)
 


--------------------------------------------------------
--------------------------------------------------------
-- Categorize feedback
-- Categories
--------------------------------------------------------
--------------------------------------------------------


Line 366: Line 589:


if resolved then
if resolved then
category =
category =
'[[Category:Resolved Feedback]]'
'[[Category:Resolved Feedback]]'
else
else
category =
category =
'[[Category:Unresolved Feedback]]'
'[[Category:Unresolved Feedback]]'
end


if id == '' then
category =
category ..
'[[Category:Feedbacks Missing ID]]'
end
end


return card .. category
return card .. category
end
end




------------------------------------------------------------
------------------------------------------------------------
-- Render feedback list
-- RENDER FEEDBACK LIST
------------------------------------------------------------
------------------------------------------------------------


local function renderFeedbackList(results)
local function renderFeedbackList(results)
if not results or #results == 0 then
if not results or #results == 0 then
return '<em>No feedback entries found.</em>'
return '<em>No feedback entries found.</em>'
end
end


local list = {}
local list = {}


for _, row in ipairs(results) do
for _, row in ipairs(results) do
table.insert(
table.insert(
list,
list,
renderCard(row, false)
renderFullCard(row)
)
)
end
end


 
return table.concat(
return table.concat(list, '\n')
list,
 
'\n'
)
end
end




------------------------------------------------------------
------------------------------------------------------------
-- View feedback
-- PAGE FEEDBACK
------------------------------------------------------------
------------------------------------------------------------


function p.view(frame)
function p.page(frame)
local page =
currentSubjectPage()


local parent = frame:getParent()
local entries =
queryFeedback(page)


local unresolved = {}
local resolved = {}


local args
for _, entry in ipairs(entries) do
 
if trim(entry.status):lower()
if frame
== 'resolved'
and frame.args
then
and frame.args.status
table.insert(
then
resolved,
 
entry
args = frame.args
)
 
else
elseif parent
table.insert(
and parent.args
unresolved,
and parent.args.status
entry
then
)
 
end
args = parent.args
 
elseif frame
and frame.args
then
 
args = frame.args
 
elseif parent
and parent.args
then
 
args = parent.args
 
else
 
args = {}
 
end
end


local root =
mw.html.create('section')
:attr(
'id',
'feedback-section'
)
:addClass('feedback-page')


return p._view(args, frame)
--------------------------------------------------------
 
-- Heading
end
--------------------------------------------------------
 


function p._view(args, frame)
root:tag('h2')
:addClass('feedback-page-title')
:wikitext('Feedbacks')


--------------------------------------------------------
--------------------------------------------------------
-- Check Bucket
-- Filters
--------------------------------------------------------
--------------------------------------------------------


if not (mw.ext and mw.ext.bucket) then
local filters =
 
root:tag('div')
return
:addClass('feedback-filters')
'<em>Bucket extension is not available.</em>'


local function addFilter(
label,
value,
active
)
return filters:tag('button')
:attr(
'type',
'button'
)
:attr(
'data-feedback-filter',
value
)
:addClass('feedback-filter')
:addClass(
active
and 'feedback-filter--active'
or nil
)
:wikitext(label)
end
end


addFilter(
'Unresolved',
'unresolved',
true
)


frame = frame or mw.getCurrentFrame()
addFilter(
'Resolved',
'resolved',
false
)


addFilter(
'All',
'all',
false
)


--------------------------------------------------------
--------------------------------------------------------
-- Determine requested status
-- Unresolved
--------------------------------------------------------
--------------------------------------------------------


local rawStatus =
local unresolvedList =
args.status
root:tag('div')
or args[1]
:addClass(
 
'feedback-page-list'
 
)
local filterStatus =
:addClass(
rawStatus
'feedback-page-list--unresolved'
and mw.text.trim(rawStatus):lower()
)
or nil
:addClass(
'feedback-page-list--visible'
)


if #unresolved == 0 then
unresolvedList:tag('div')
:addClass('feedback-empty')
:wikitext(
'No unresolved feedback.'
)
else
for _, entry in ipairs(unresolved) do
unresolvedList:wikitext(
renderCompactCard(entry)
)
end
end


--------------------------------------------------------
--------------------------------------------------------
-- Specific status
-- Resolved
--------------------------------------------------------
--------------------------------------------------------


if filterStatus == 'resolved'
local resolvedList =
or filterStatus == 'unresolved'
root:tag('div')
then
:addClass(
 
'feedback-page-list'
local query =
)
mw.ext.bucket('feedback')
:addClass(
 
'feedback-page-list--resolved'
 
)
query.select(
'page',
'user',
'status',
'feedback'
)
 
 
query.where(
'status',
filterStatus
)
 
 
return renderFeedbackList(
query.run()
)


if #resolved == 0 then
resolvedList:tag('div')
:addClass('feedback-empty')
:wikitext(
'No resolved feedback.'
)
else
for _, entry in ipairs(resolved) do
resolvedList:wikitext(
renderCompactCard(entry)
)
end
end
end


--------------------------------------------------------
--------------------------------------------------------
-- All feedback
-- All
--------------------------------------------------------
--------------------------------------------------------


if filterStatus == 'all' then
local allList =
root:tag('div')
:addClass(
'feedback-page-list'
)
:addClass(
'feedback-page-list--all'
)


local query =
if #entries == 0 then
mw.ext.bucket('feedback')
allList:tag('div')
:addClass('feedback-empty')
:wikitext(
'No feedback.'
)
else
for _, entry in ipairs(entries) do
allList:wikitext(
renderCompactCard(entry)
)
end
end


return tostring(root)
end


query.select(
'page',
'user',
'status',
'feedback'
)


------------------------------------------------------------
-- COUNTS
------------------------------------------------------------


return renderFeedbackList(
function p.counts(frame)
query.run()
local entries =
queryFeedback(
currentSubjectPage()
)
)


local unresolved = 0
local resolved = 0
for _, entry in ipairs(entries) do
if trim(entry.status):lower()
== '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


--------------------------------------------------------
-- Tabber mode
--------------------------------------------------------


local qAll =
------------------------------------------------------------
mw.ext.bucket('feedback')
-- TABLE / DASHBOARD
------------------------------------------------------------


local function renderTable(entries)
if not entries or #entries == 0 then
return '<em>No feedback entries found.</em>'
end


qAll.select(
local table =
'page',
mw.html.create('table')
'user',
:addClass('feedback-table')
'status',
'feedback'
)


local head =
table:tag('thead')
:tag('tr')


local allEntries =
head:tag('th')
qAll.run()
:wikitext('Status')


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


local allList = {}
head:tag('th')
local unresolvedList = {}
:wikitext('Feedback')
local resolvedList = {}


head:tag('th')
:wikitext('User')


--------------------------------------------------------
head:tag('th')
-- Split entries by status
:wikitext('Date')
--------------------------------------------------------


if allEntries and #allEntries > 0 then
local body =
table:tag('tbody')


for _, row in ipairs(allEntries) do
for _, entry in ipairs(entries) do
local row =
body:tag('tr')


local card =
local resolved =
renderCard(row, false)
trim(entry.status):lower()
== 'resolved'


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


table.insert(
row:tag('td')
allList,
:addClass('feedback-table-page')
card
:wikitext(
entry.page or ''
)
)


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


if row.status == 'resolved' then
row:tag('td')
:wikitext(
formatUser(entry.user)
)


table.insert(
row:tag('td')
resolvedList,
:wikitext(
card
entry.createddate or ''
)
)
end


else
return tostring(table)
end


table.insert(
unresolvedList,
card
)


end
function p.view(frame)
local parent =
frame:getParent()


end
local args


end
if frame
and frame.args
and frame.args.status
then
args = frame.args


elseif parent
and parent.args
and parent.args.status
then
args = parent.args


--------------------------------------------------------
elseif frame
-- Build tab contents
and frame.args
--------------------------------------------------------
then
args = frame.args


local allContent
elseif parent
and parent.args
then
args = parent.args


if #allList > 0 then
else
args = {}
end


allContent =
return p._view(
table.concat(allList, '\n')
args,
frame
)
end


else


allContent =
function p._view(args, frame)
'<em>No feedback entries found.</em>'
local bucket =
getBucket()


if not bucket then
return
'<em>Bucket extension is not available.</em>'
end
end


local rawStatus =
args.status
or args[1]


local unresolvedContent
local filterStatus =
rawStatus
and trim(rawStatus):lower()
or nil


if #unresolvedList > 0 then
--------------------------------------------------------
-- Query
--------------------------------------------------------


unresolvedContent =
local query =
table.concat(unresolvedList, '\n')
bucket


else
query.select(
 
'page',
unresolvedContent =
'user',
'<em>No unresolved feedback entries found.</em>'
'status',
'feedback',
'id',
'createddate',
'link',
'image',
'resolvedby',
'resolveddate'
)


if filterStatus == 'resolved'
or filterStatus == 'unresolved'
then
query.where(
'status',
filterStatus
)
end
end


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


local resolvedContent
if not success or not entries then
return
'<em>Could not load feedback.</em>'
end


if #resolvedList > 0 then
entries =
sortNewestFirst(entries)


resolvedContent =
--------------------------------------------------------
table.concat(resolvedList, '\n')
-- Specific status
 
--------------------------------------------------------
else
 
resolvedContent =
'<em>No resolved feedback entries found.</em>'


if filterStatus == 'resolved'
or filterStatus == 'unresolved'
then
return renderFeedbackList(
entries
)
end
end


--------------------------------------------------------
--------------------------------------------------------
-- Tabber
-- All
--------------------------------------------------------
--------------------------------------------------------


local tabberBody = string.format(
return renderTable(entries)
'All=\n%s\n|-|Unresolved=\n%s\n|-|Resolved=\n%s',
end
allContent,
unresolvedContent,
resolvedContent
)
 
 
return frame:callParserFunction(
'#tag',
{
'tabber',
tabberBody
}
)


end


p.table = p.view


return p
return p

Latest revision as of 13:57, 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 == 'true'
		or value == 'yes'
		or value == '1'
		or value == 'resolved'
end


local function trim(value)
	if value == nil then
		return ''
	end

	return mw.text.trim(tostring(value))
end


local function formatUser(user)
	user = trim(user)

	if user == '' then
		return 'Anonymous'
	end

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


local function currentSubjectPage()
	local title = mw.title.getCurrentTitle()

	if title.subjectPageTitle then
		return title.subjectPageTitle.fullText
	end

	return title.fullText
end


local function getBucket()
	if not (mw.ext and mw.ext.bucket) then
		return nil
	end

	return mw.ext.bucket('feedback')
end


local function sortNewestFirst(entries)
	table.sort(
		entries,
		function(a, b)
			local aDate = trim(a.createddate)
			local bDate = trim(b.createddate)

			return aDate > bDate
		end
	)

	return entries
end


local function queryFeedback(page)
	local bucket = getBucket()

	if not bucket then
		return {}
	end

	local query = bucket

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

	query.where(
		'page',
		page
	)

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

	if not success or not results then
		return {}
	end

	return sortNewestFirst(results)
end


------------------------------------------------------------
-- ATTACHMENTS
------------------------------------------------------------

local function renderAttachments(entry)
	local wrapper =
		mw.html.create('div')
			:addClass('feedback-attachments')

	local link = trim(entry.link)

	if link ~= '' then
		local linkWrapper =
			wrapper:tag('span')
				:addClass('feedback-attachment-link')
	
		linkWrapper:tag('span')
			:addClass('feedback-attachment-icon')
	
		linkWrapper:wikitext(
			'[' .. link .. ' ' .. mw.text.nowiki(link) .. ']'
		)
	end

	local image = trim(entry.image)

	if image ~= '' then
		image = mw.ustring.gsub(
			image,
			'^File:',
			''
		)

		local title =
			mw.title.new('File:' .. image)

		if title then
			local imageWrapper =
				wrapper:tag('span')
					:addClass('feedback-attachment-image')
		
			imageWrapper:tag('span')
				:addClass('feedback-attachment-icon')
		
			imageWrapper:wikitext(
				'[' ..
				title:fullUrl() ..
				' ' ..
				mw.text.nowiki(image) ..
				']'
			)
		end
	end

	return wrapper
end


------------------------------------------------------------
-- RESOLUTION
------------------------------------------------------------

local function renderResolution(parent, entry)
	local resolvedBy = trim(entry.resolvedby)
	local resolvedDate = trim(entry.resolveddate)

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

	local resolution =
		parent: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 trim(id) == '' then
		return
	end

	local wrapper =
		parent:tag('div')
			:addClass('feedback-resolve')

	local control =
		wrapper:tag('span')
			:addClass('feedback-resolve-button')
			:attr(
				'data-feedback-id',
				id
			)
			:attr(
				'data-feedback-page',
				page or ''
			)
			:attr(
				'role',
				'button'
			)
			:attr(
				'tabindex',
				'0'
			)
			:attr(
				'aria-label',
				'Resolve feedback'
			)

	if compact then
		control
			:addClass(
				'feedback-resolve-button--compact'
			)
			:attr(
				'title',
				'Resolve feedback'
			)
			:wikitext('✓')
	else
		control:wikitext('Resolve')
	end
end


------------------------------------------------------------
-- FULL FEEDBACK CARD
------------------------------------------------------------

local function renderFullCard(entry)
	local resolved =
		trim(entry.status):lower() == 'resolved'

	local id = trim(entry.id)
	local page = trim(entry.page)
	local feedback = trim(entry.feedback)
	local user = trim(entry.user)
	local createddate = trim(entry.createddate)

	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 content column
	--------------------------------------------------------

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

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

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

	--------------------------------------------------------
	-- Status
	--------------------------------------------------------

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

	--------------------------------------------------------
	-- Feedback text
	--------------------------------------------------------

	main:tag('span')
		:addClass('feedback-text')
		:wikitext(
			feedback ~= ''
				and feedback
				or 'No feedback provided'
		)

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

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

	if user ~= '' then
		meta:wikitext(
			' · ' .. formatUser(user)
		)
	end

	if createddate ~= '' then
		meta:wikitext(
			' · ' .. mw.text.nowiki(createddate)
		)
	end

	--------------------------------------------------------
	-- Attachments
	--------------------------------------------------------
	
	content:node(
	    renderAttachments(entry)
	)

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

	renderResolution(
		content,
		entry
	)

	--------------------------------------------------------
	-- Missing ID warning
	--------------------------------------------------------

	if id == '' then
		content:tag('div')
			:addClass('feedback-missing-id')
			:wikitext(
				'This feedback entry is missing an ID.'
			)
	end

	--------------------------------------------------------
	-- Resolve button
	--
	-- IMPORTANT:
	-- This is outside .feedback-content so it
	-- remains on the right.
	--------------------------------------------------------

	if not resolved then
		renderResolveControl(
			root,
			id,
			page,
			false
		)
	end

	return tostring(root)
end


------------------------------------------------------------
-- COMPACT CARD
------------------------------------------------------------

local function renderCompactCard(entry)
	return renderFullCard(entry)
end


------------------------------------------------------------
-- SAVE FEEDBACK
------------------------------------------------------------

local function saveFeedback(data)
	local bucket = getBucket()

	if not bucket then
		return false, 'Bucket extension is not available'
	end

	local success, err = 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(err)
	end

	return true
end


------------------------------------------------------------
-- MAIN
--
-- Used by Template:Feedback
------------------------------------------------------------

function p.main(frame)
	local parent = frame:getParent()

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

	local pageTitle = nil

	if parent and parent.getTitle then
		local title = parent:getTitle()

		if title
			and title ~= ''
			and not title:find('^Template:')
		then
			pageTitle = title
		end
	end

	return p._main(
		args,
		pageTitle
	)
end


function p._main(args, pageTitle)
	local feedback =
		trim(args.feedback)

	if feedback == '' then
		feedback = 'No feedback provided'
	end

	local user =
		trim(args.user)

	if user == '' then
		user = 'Anonymous'
	end

	local resolved =
		isTruthy(args.resolved)

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

	local currentPage =
		trim(pageTitle)

	if currentPage == '' then
		currentPage =
			mw.title.getCurrentTitle().fullText
	end

	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)

	--------------------------------------------------------
	-- Save to Bucket
	--------------------------------------------------------

	local saved, saveError =
		saveFeedback({
			page = currentPage,
			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

	--------------------------------------------------------
	-- Render
	--------------------------------------------------------

	local card =
		renderFullCard({
			page = currentPage,
			user = user,
			status = status,
			feedback = feedback,
			id = id,
			createddate = createddate,
			link = link,
			image = image,
			resolvedby = resolvedby,
			resolveddate = resolveddate
		})

	--------------------------------------------------------
	-- Categories
	--------------------------------------------------------

	local category

	if resolved then
		category =
			'[[Category:Resolved Feedback]]'
	else
		category =
			'[[Category:Unresolved Feedback]]'
	end

	if id == '' then
		category =
			category ..
			'[[Category:Feedbacks Missing ID]]'
	end

	return card .. category
end


------------------------------------------------------------
-- RENDER FEEDBACK LIST
------------------------------------------------------------

local function renderFeedbackList(results)
	if not results or #results == 0 then
		return '<em>No feedback entries found.</em>'
	end

	local list = {}

	for _, row in ipairs(results) do
		table.insert(
			list,
			renderFullCard(row)
		)
	end

	return table.concat(
		list,
		'\n'
	)
end


------------------------------------------------------------
-- PAGE FEEDBACK
------------------------------------------------------------

function p.page(frame)
	local page =
		currentSubjectPage()

	local entries =
		queryFeedback(page)

	local unresolved = {}
	local resolved = {}

	for _, entry in ipairs(entries) do
		if trim(entry.status):lower()
			== 'resolved'
		then
			table.insert(
				resolved,
				entry
			)
		else
			table.insert(
				unresolved,
				entry
			)
		end
	end

	local root =
		mw.html.create('section')
			:attr(
				'id',
				'feedback-section'
			)
			:addClass('feedback-page')

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

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

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

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

	local function addFilter(
		label,
		value,
		active
	)
		return filters:tag('button')
			:attr(
				'type',
				'button'
			)
			:attr(
				'data-feedback-filter',
				value
			)
			:addClass('feedback-filter')
			:addClass(
				active
					and 'feedback-filter--active'
					or nil
			)
			:wikitext(label)
	end

	addFilter(
		'Unresolved',
		'unresolved',
		true
	)

	addFilter(
		'Resolved',
		'resolved',
		false
	)

	addFilter(
		'All',
		'all',
		false
	)

	--------------------------------------------------------
	-- Unresolved
	--------------------------------------------------------

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

	if #unresolved == 0 then
		unresolvedList:tag('div')
			:addClass('feedback-empty')
			:wikitext(
				'No unresolved feedback.'
			)
	else
		for _, entry in ipairs(unresolved) do
			unresolvedList:wikitext(
				renderCompactCard(entry)
			)
		end
	end

	--------------------------------------------------------
	-- Resolved
	--------------------------------------------------------

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

	if #resolved == 0 then
		resolvedList:tag('div')
			:addClass('feedback-empty')
			:wikitext(
				'No resolved feedback.'
			)
	else
		for _, entry in ipairs(resolved) do
			resolvedList:wikitext(
				renderCompactCard(entry)
			)
		end
	end

	--------------------------------------------------------
	-- All
	--------------------------------------------------------

	local allList =
		root:tag('div')
			:addClass(
				'feedback-page-list'
			)
			:addClass(
				'feedback-page-list--all'
			)

	if #entries == 0 then
		allList:tag('div')
			:addClass('feedback-empty')
			:wikitext(
				'No feedback.'
			)
	else
		for _, entry in ipairs(entries) do
			allList:wikitext(
				renderCompactCard(entry)
			)
		end
	end

	return tostring(root)
end


------------------------------------------------------------
-- COUNTS
------------------------------------------------------------

function p.counts(frame)
	local entries =
		queryFeedback(
			currentSubjectPage()
		)

	local unresolved = 0
	local resolved = 0

	for _, entry in ipairs(entries) do
		if trim(entry.status):lower()
			== '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


------------------------------------------------------------
-- TABLE / DASHBOARD
------------------------------------------------------------

local function renderTable(entries)
	if not entries or #entries == 0 then
		return '<em>No feedback entries found.</em>'
	end

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

	local head =
		table:tag('thead')
			:tag('tr')

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

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

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

	head:tag('th')
		:wikitext('User')

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

	local body =
		table:tag('tbody')

	for _, entry in ipairs(entries) do
		local row =
			body:tag('tr')

		local resolved =
			trim(entry.status):lower()
				== 'resolved'

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

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

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

		row:tag('td')
			:wikitext(
				formatUser(entry.user)
			)

		row:tag('td')
			:wikitext(
				entry.createddate or ''
			)
	end

	return tostring(table)
end


function p.view(frame)
	local parent =
		frame:getParent()

	local args

	if frame
		and frame.args
		and frame.args.status
	then
		args = frame.args

	elseif parent
		and parent.args
		and parent.args.status
	then
		args = parent.args

	elseif frame
		and frame.args
	then
		args = frame.args

	elseif parent
		and parent.args
	then
		args = parent.args

	else
		args = {}
	end

	return p._view(
		args,
		frame
	)
end


function p._view(args, frame)
	local bucket =
		getBucket()

	if not bucket then
		return
			'<em>Bucket extension is not available.</em>'
	end

	local rawStatus =
		args.status
		or args[1]

	local filterStatus =
		rawStatus
		and trim(rawStatus):lower()
		or nil

	--------------------------------------------------------
	-- Query
	--------------------------------------------------------

	local query =
		bucket

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

	if filterStatus == 'resolved'
		or filterStatus == 'unresolved'
	then
		query.where(
			'status',
			filterStatus
		)
	end

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

	if not success or not entries then
		return
			'<em>Could not load feedback.</em>'
	end

	entries =
		sortNewestFirst(entries)

	--------------------------------------------------------
	-- Specific status
	--------------------------------------------------------

	if filterStatus == 'resolved'
		or filterStatus == 'unresolved'
	then
		return renderFeedbackList(
			entries
		)
	end

	--------------------------------------------------------
	-- All
	--------------------------------------------------------

	return renderTable(entries)
end


p.table = p.view

return p