Jump to content

Module:Feedback

From The Petit Planet Wiki
Revision as of 12:54, 16 September 2026 by ReisuDesign (talk | contribs) (Undo revision 24119 by ReisuDesign (talk))
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