Jump to content

Module:Feedback: Difference between revisions

From The Petit Planet Wiki
ReisuDesign (talk | contribs)
Created page with "local p = {} ------------------------------------------------------------ -- Helpers ------------------------------------------------------------ local function isTruthy(v) if v == nil then return false end v = mw.text.trim(tostring(v)):lower() return v ~= '' and v ~= '0' and v ~= 'false' and v ~= 'no' and v ~= 'off' end local function formatUser(user) user = mw.text.trim(tostring(user or '')) if user..."
 
ReisuDesign (talk | contribs)
No edit summary
 
(20 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


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




------------------------------------------------------------
local function currentSubjectPage()
-- Feedback card
local title = mw.title.getCurrentTitle()
------------------------------------------------------------


local function renderCard(entry, showInstruction)
if title.subjectPageTitle then
return title.subjectPageTitle.fullText
end


    local resolved = (entry.status == 'resolved')
return title.fullText
end


    local user = entry.user or 'Anonymous'
    local feedback = entry.feedback or 'No feedback provided'
    local page = entry.page


    local root = mw.html.create('div')
local function getBucket()
        :addClass('feedback-box')
if not (mw.ext and mw.ext.bucket) then
        :addClass(
return nil
            resolved
end
                and 'feedback-box--resolved'
                or 'feedback-box--unresolved'
        )


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


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


    local meta = root:tag('div')
local function sortNewestFirst(entries)
        :addClass('feedback-meta')
table.sort(
entries,
function(a, b)
local aDate = trim(a.createddate)
local bDate = trim(b.createddate)


return aDate > bDate
end
)


    if page and page ~= '' then
return entries
end


        meta:tag('span')
            :addClass('feedback-page')
            :wikitext(
                string.format('[[%s]]', page)
            )


        meta:tag('span')
local function queryFeedback(page)
            :addClass('feedback-separator')
local bucket = getBucket()
            :wikitext(' • ')


    end
if not bucket then
return {}
end


local query = bucket


    meta:tag('span')
query.select(
        :addClass('feedback-user')
'page',
        :wikitext(formatUser(user))
'user',
'status',
'feedback',
'id',
'createddate',
'link',
'image',
'resolvedby',
'resolveddate'
)


query.where(
'page',
page
)


    meta:tag('span')
local success, results = pcall(function()
        :addClass('feedback-separator')
return query.run()
        :wikitext(' • ')
end)


if not success or not results then
return {}
end


    meta:tag('span')
return sortNewestFirst(results)
        :addClass('feedback-status')
end
        :addClass(
            resolved
                and 'feedback-status--resolved'
                or 'feedback-status--unresolved'
        )
        :wikitext(
            resolved
                and 'Resolved'
                or 'Unresolved'
        )




    --------------------------------------------------------
------------------------------------------------------------
    -- Feedback text
-- ATTACHMENTS
    --------------------------------------------------------
------------------------------------------------------------


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


local link = trim(entry.link)


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


    if showInstruction and not resolved then
local image = trim(entry.image)


        root:tag('div')
if image ~= '' then
            :addClass('feedback-instruction')
image = mw.ustring.gsub(
            :wikitext(
image,
                'If this feedback has been resolved, edit this page '
'^File:',
                .. 'and set <code>|resolved=true</code>.'
''
            )
)


    end
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 tostring(root)
return wrapper
end
end




------------------------------------------------------------
------------------------------------------------------------
-- Save feedback
-- RESOLUTION
------------------------------------------------------------
------------------------------------------------------------


local function saveFeedback(data)
local function renderResolution(parent, entry)
 
local resolvedBy = trim(entry.resolvedby)
    if not (mw.ext and mw.ext.bucket) then
local resolvedDate = trim(entry.resolveddate)
        return false
    end
 
    local bucket = mw.ext.bucket('feedback')
 
    if not bucket then
        return false
    end


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


    local success = pcall(function()
local resolution =
parent:tag('div')
:addClass('feedback-resolution')


        bucket.put({
resolution:wikitext('Resolved by ')
            page = data.page,
            user = data.user,
            status = data.status,
            feedback = data.feedback
        })


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


 
if resolvedDate ~= '' then
    return success
resolution:wikitext(
' on ' .. mw.text.nowiki(resolvedDate)
)
end
end
end




------------------------------------------------------------
------------------------------------------------------------
-- Main
-- RESOLVE CONTROL
--
-- Used by Template:Feedback
--
-- Example:
--
-- {{Feedback
--  |feedback=Something could be improved here.
--  |user=Rei
--  |resolved=false
-- }}
------------------------------------------------------------
------------------------------------------------------------


function p.main(frame)
local function renderResolveControl(
parent,
id,
page,
compact
)
if trim(id) == '' then
return
end


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


    local args =
local control =
        parent and parent.args
wrapper:tag('span')
        or frame.args
: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


    --------------------------------------------------------
    -- Get page title
    --------------------------------------------------------


    local pageTitle = nil
------------------------------------------------------------
-- FULL FEEDBACK CARD
------------------------------------------------------------


    if parent and parent.getTitle then
local function renderFullCard(entry)
local resolved =
trim(entry.status):lower() == 'resolved'


        local title = parent:getTitle()
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)


        if title
local root =
            and title ~= ''
mw.html.create('div')
            and not title:find('^Template:')
:addClass('feedback-box')
        then
:addClass(
            pageTitle = title
resolved
        end
and 'feedback-box--resolved'
or 'feedback-box--unresolved'
)


    end
if id ~= '' then
root:attr(
'data-feedback-id',
id
)
end


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


    return p._main(args, pageTitle)
local content =
root:tag('div')
:addClass('feedback-content')


end
--------------------------------------------------------
-- Main line
--------------------------------------------------------


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


function p._main(args, pageTitle)
--------------------------------------------------------
-- Status
--------------------------------------------------------


    local feedback =
main:tag('span')
        args.feedback
:addClass('feedback-status')
        or 'No feedback provided'
:addClass(
resolved
and 'feedback-status--resolved'
or 'feedback-status--unresolved'
)
:wikitext(
resolved
and 'Resolved'
or 'Unresolved'
)


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


    local user =
main:tag('span')
        args.user
:addClass('feedback-text')
        or 'Anonymous'
:wikitext(
feedback ~= ''
and feedback
or 'No feedback provided'
)


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


    local resolved =
local meta =
        isTruthy(args.resolved)
main:tag('span')
:addClass('feedback-inline-meta')


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


    local status =
if createddate ~= '' then
        resolved
meta:wikitext(
            and 'resolved'
' · ' .. mw.text.nowiki(createddate)
            or 'unresolved'
)
end


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


    local currentPage =
--------------------------------------------------------
        pageTitle
-- Resolution information
        or mw.title.getCurrentTitle().fullText
--------------------------------------------------------


renderResolution(
content,
entry
)


    --------------------------------------------------------
--------------------------------------------------------
    -- Save to Bucket
-- Missing ID warning
    --------------------------------------------------------
--------------------------------------------------------


    saveFeedback({
if id == '' then
        page = currentPage,
content:tag('div')
        user = user,
:addClass('feedback-missing-id')
        status = status,
:wikitext(
        feedback = feedback
'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
    -- Render the feedback card
renderResolveControl(
    --------------------------------------------------------
root,
id,
page,
false
)
end


    local card = renderCard({
return tostring(root)
end


        page = nil,


        user = user,
------------------------------------------------------------
-- COMPACT CARD
------------------------------------------------------------


        status = status,
local function renderCompactCard(entry)
return renderFullCard(entry)
end


        feedback = feedback


    }, true)
------------------------------------------------------------
-- SAVE FEEDBACK
------------------------------------------------------------


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


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


    local category
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 resolved then
if not success then
 
return false, tostring(err)
        category =
end
            '[[Category:Resolved Feedback]]'
 
    else
 
        category =
            '[[Category:Unresolved Feedback]]'
 
    end
 
 
    return card .. category


return true
end
end




------------------------------------------------------------
------------------------------------------------------------
-- Render feedback list
-- MAIN
--
-- Used by Template:Feedback
------------------------------------------------------------
------------------------------------------------------------


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


    if not results or #results == 0 then
local args =
        return '<em>No feedback entries found.</em>'
parent and parent.args
    end
or frame.args


local pageTitle = nil


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


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


    for _, row in ipairs(results) do
return p._main(
args,
pageTitle
)
end


        table.insert(
            list,
            renderCard(row, false)
        )


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


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


    return table.concat(list, '\n')
local user =
trim(args.user)


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


local resolved =
isTruthy(args.resolved)


------------------------------------------------------------
local status =
-- View feedback
resolved
--
and 'resolved'
-- Examples:
or 'unresolved'
--
-- {{#invoke:Feedback|view}}
--
-- {{#invoke:Feedback|view|status=all}}
--
-- {{#invoke:Feedback|view|status=resolved}}
--
-- {{#invoke:Feedback|view|status=unresolved}}
------------------------------------------------------------


function p.view(frame)
local currentPage =
trim(pageTitle)


    local parent = frame:getParent()
if currentPage == '' then
currentPage =
mw.title.getCurrentTitle().fullText
end


local id =
trim(args.id)


    local args
local createddate =
trim(args.createddate)


    if frame
local link =
        and frame.args
trim(args.link)
        and frame.args.status
    then


        args = frame.args
local image =
trim(args.image)


    elseif parent
local resolvedby =
        and parent.args
trim(args.resolvedby)
        and parent.args.status
    then


        args = parent.args
local resolveddate =
trim(args.resolveddate)


    elseif frame
--------------------------------------------------------
        and frame.args
-- Save to Bucket
    then
--------------------------------------------------------


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


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


        args = parent.args
--------------------------------------------------------
-- Render
--------------------------------------------------------


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


        args = {}
--------------------------------------------------------
-- Categories
--------------------------------------------------------


    end
local category


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


    return p._view(args, frame)
if id == '' then
category =
category ..
'[[Category:Feedbacks Missing ID]]'
end


return card .. category
end
end




function p._view(args, frame)
------------------------------------------------------------
-- RENDER FEEDBACK LIST
------------------------------------------------------------


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


    if not (mw.ext and mw.ext.bucket) then
local list = {}


        return
for _, row in ipairs(results) do
            '<em>Bucket extension is not available.</em>'
table.insert(
list,
renderFullCard(row)
)
end


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




    frame = frame or mw.getCurrentFrame()
------------------------------------------------------------
-- PAGE FEEDBACK
------------------------------------------------------------


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


    --------------------------------------------------------
local entries =
    -- Determine requested status
queryFeedback(page)
    --------------------------------------------------------


    local rawStatus =
local unresolved = {}
        args.status
local resolved = {}
        or args[1]


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 filterStatus =
local root =
        rawStatus
mw.html.create('section')
        and mw.text.trim(rawStatus):lower()
:attr(
        or nil
'id',
'feedback-section'
)
:addClass('feedback-page')


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


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


    if filterStatus == 'resolved'
--------------------------------------------------------
        or filterStatus == 'unresolved'
-- Filters
    then
--------------------------------------------------------


        local query =
local filters =
            mw.ext.bucket('feedback')
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


        query.select(
addFilter(
            'page',
'Unresolved',
            'user',
'unresolved',
            'status',
true
            'feedback'
)
        )


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


        query.where(
addFilter(
            'status',
'All',
            filterStatus
'all',
        )
false
)


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


        return renderFeedbackList(
local unresolvedList =
            query.run()
root:tag('div')
        )
:addClass(
'feedback-page-list'
)
:addClass(
'feedback-page-list--unresolved'
)
:addClass(
'feedback-page-list--visible'
)


    end
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 =
    -- All feedback
root:tag('div')
    --------------------------------------------------------
:addClass(
'feedback-page-list'
)
:addClass(
'feedback-page-list--resolved'
)


    if filterStatus == 'all' then
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


        local query =
--------------------------------------------------------
            mw.ext.bucket('feedback')
-- All
--------------------------------------------------------


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


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


return tostring(root)
end


        return renderFeedbackList(
            query.run()
        )


    end
------------------------------------------------------------
-- COUNTS
------------------------------------------------------------


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


    --------------------------------------------------------
local unresolved = 0
    -- Tabber mode
local resolved = 0
    --
    -- Default:
    --
    -- All
    -- Unresolved
    -- Resolved
    --------------------------------------------------------


    local qAll =
for _, entry in ipairs(entries) do
        mw.ext.bucket('feedback')
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


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


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


    local allEntries =
local function renderTable(entries)
        qAll.run()
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 allList = {}
local head =
    local unresolvedList = {}
table:tag('thead')
    local resolvedList = {}
:tag('tr')


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


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


    if allEntries and #allEntries > 0 then
head:tag('th')
:wikitext('Feedback')


        for _, row in ipairs(allEntries) do
head:tag('th')
:wikitext('User')


            local card =
head:tag('th')
                renderCard(row, false)
:wikitext('Date')


local body =
table:tag('tbody')


            table.insert(
for _, entry in ipairs(entries) do
                allList,
local row =
                card
body:tag('tr')
            )


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


            if row.status == 'resolved' then
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')
                    resolvedList,
:addClass('feedback-table-page')
                    card
:wikitext(
                )
entry.page or ''
)


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


                table.insert(
row:tag('td')
                    unresolvedList,
:wikitext(
                    card
formatUser(entry.user)
                )
)


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


        end
return tostring(table)
end


    end


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


    --------------------------------------------------------
local args
    -- Build tab contents
    --------------------------------------------------------


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


    if #allList > 0 then
elseif parent
and parent.args
and parent.args.status
then
args = parent.args


        allContent =
elseif frame
            table.concat(allList, '\n')
and frame.args
then
args = frame.args


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


        allContent =
else
            '<em>No feedback entries found.</em>'
args = {}
end


    end
return p._view(
args,
frame
)
end




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


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


        unresolvedContent =
local rawStatus =
            table.concat(unresolvedList, '\n')
args.status
or args[1]


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


        unresolvedContent =
--------------------------------------------------------
            '<em>No unresolved feedback entries found.</em>'
-- Query
--------------------------------------------------------


    end
local query =
bucket


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


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


    if #resolvedList > 0 then
local success, entries =
pcall(function()
return query.run()
end)


        resolvedContent =
if not success or not entries then
            table.concat(resolvedList, '\n')
return
'<em>Could not load feedback.</em>'
end


    else
entries =
sortNewestFirst(entries)


        resolvedContent =
--------------------------------------------------------
            '<em>No resolved feedback entries found.</em>'
-- Specific status
--------------------------------------------------------


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


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


    --------------------------------------------------------
return renderTable(entries)
    -- Tabber
end
    --------------------------------------------------------


    local tabberBody = string.format(
        'All=\n%s\n|-|Unresolved=\n%s\n|-|Resolved=\n%s',
        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