mw.loader
.using([
'vue',
'@wikimedia/codex',
'mediawiki.ForeignApi',
'mediawiki.util',
'mediawiki.storage',
])
.then(function (require) {
const allowedNamespaces = [0, 4, 12, 14, 100]; // Məqalə, Vikipediya, Kömək, Kateqoriya, Portal
if (!allowedNamespaces.includes(mw.config.get('wgNamespaceNumber'))) return;
if (mw.config.get('wgAction') !== 'view') return;
const { createMwApp } = require('vue');
const {
CdxButton,
CdxDialog,
CdxProgressBar,
CdxIcon,
CdxCheckbox,
CdxMessage,
CdxTextInput,
CdxToggleSwitch,
CdxTabs,
CdxTab,
CdxProgressIndicator,
} = require('@wikimedia/codex');
const api = new mw.Api({ userAgent: 'CatSuggest/azwiki' });
const wikidataApi = new mw.ForeignApi(
'https://www.wikidata.org/w/api.php',
{ userAgent: 'CatSuggest/azwiki' },
);
const app = createMwApp({
data: () => ({
dialogShown: false,
loading: false,
suggestions: [],
qid: mw.config.get('wgWikibaseItemId'),
status: 'idle', // idle, loading, results, editing, success
selectedSuggestions: [],
error: null,
sitelinks: [],
modeSelectedSites: {
members: [],
parentCats: [],
},
siteFilter: '',
debouncedSiteFilter: '',
siteFilterTimeout: null,
mode:
mw.config.get('wgNamespaceNumber') === 14 ? 'members' : 'parentCats', // Default to members for Categories, parentCats for others
isCategoryPage: mw.config.get('wgNamespaceNumber') === 14,
localStatePromise: null,
currentProcessingTitle: '',
processedCount: 0,
totalProcessingCount: 0,
}),
computed: {
canFetch() {
return this.selectedSites.length > 0 && !this.loading;
},
allSelected: {
get() {
return (
this.sitelinks.length > 0 &&
this.selectedSites.length === this.sitelinks.length
);
},
set(value) {
if (value) {
this.selectedSites = this.sitelinks.map((s) => s.site);
} else {
this.selectedSites = [];
}
},
},
showSearch() {
return this.sitelinks.length > 10;
},
filteredSitelinks() {
if (!this.debouncedSiteFilter) return this.sitelinks;
const filter = this.debouncedSiteFilter.toLowerCase();
return this.sitelinks.filter(
(s) =>
s.name.toLowerCase().includes(filter) ||
s.lang.toLowerCase().includes(filter),
);
},
selectedSites: {
get() {
return this.modeSelectedSites[this.mode];
},
set(value) {
this.modeSelectedSites[this.mode] = value;
},
},
gridStyle() {
const count = this.sitelinks.length;
const columns = Math.max(2, Math.min(3, Math.ceil(count / 10)));
return {
display: 'grid',
gridTemplateColumns: `repeat(${columns}, 1fr)`,
gap: '8px',
padding: '8px',
border: '1px solid var(--border-color-base, #a2a9b1)',
borderRadius: '2px',
maxHeight: '200px',
overflowY: 'auto',
};
},
suggestedPages() {
return this.suggestions.filter((s) => !s.isCategory);
},
selectedCount() {
return this.suggestions.filter((s) => s.selected).length;
},
suggestedSubcats() {
return this.suggestions.filter((s) => s.isCategory);
},
estimatedTime() {
const count = this.selectedSites.length;
if (count === 0) return 0;
// 1s baseline for pre-fetch + wikidata + redundancy check
// + (number of batches - 1) * 0.5s sleep
// + overhead per batch
const batches = Math.ceil(count / 3);
const sleepTime = (batches - 1) * 0.5;
const networkTime = batches * 0.3 + 0.8;
return Math.ceil(sleepTime + networkTime);
},
},
methods: {
toggleDialog() {
this.dialogShown = !this.dialogShown;
if (this.dialogShown) {
if (this.sitelinks.length === 0) {
this.initGadget();
}
this.fetchLocalState();
}
},
async initGadget() {
if (!this.qid) {
this.error = 'Bu səhifə Vikidatada tapılmadı.';
return;
}
this.loading = true;
this.status = 'loading';
try {
if (!this.qid) {
this.error = 'Bu səhifə Vikidatada tapılmadı.';
this.status = 'idle';
return;
}
const wdResp = await wikidataApi.get({
action: 'wbgetentities',
ids: this.qid,
props: 'sitelinks',
format: 'json',
});
const entity = wdResp.entities[this.qid];
if (!entity || !entity.sitelinks) {
this.error = 'Bu səhifənin interviki keçidi mövcud deyil.';
this.status = 'idle';
return;
}
const cacheKeyLang = 'catsuggest-languages-map';
let langMap = {};
const cachedLang = mw.storage.get(cacheKeyLang);
if (cachedLang) {
try {
langMap = JSON.parse(cachedLang);
} catch (e) {
mw.storage.remove(cacheKeyLang);
}
}
if (Object.keys(langMap).length === 0) {
const langResp = await api.get({
action: 'query',
meta: 'siteinfo',
siprop: 'languages',
format: 'json',
});
if (langResp.query && langResp.query.languages) {
langResp.query.languages.forEach((l) => {
langMap[l.code] = l['*'];
});
// Cache for 365 days (31536000 seconds)
mw.storage.set(cacheKeyLang, JSON.stringify(langMap), 31536000);
}
}
const excludedSites = [
'commonswiki',
'metawiki',
'specieswiki',
'wikidatawiki',
'incubatorwiki',
'foundationwiki',
'testwiki',
'test2wiki',
'sourceswiki',
];
this.sitelinks = Object.values(entity.sitelinks)
.filter(
(s) =>
s.site.endsWith('wiki') &&
s.site !== 'azwiki' &&
!excludedSites.includes(s.site),
)
.map((s) => {
const langCode = s.site
.replace(/wiki$/, '')
.replaceAll('_', '-');
return {
site: s.site,
title: s.title,
lang: langCode,
name: langMap[langCode] || langCode,
};
});
this.sitelinks.sort((a, b) => a.lang.localeCompare(b.lang));
if (this.sitelinks.length === 0) {
this.error = 'Digər dillərdə uyğun məqalə tapılmadı.';
this.status = 'idle';
return;
}
// Default to enwiki and ruwiki if available
const defaultSites = [];
if (this.sitelinks.some((s) => s.site === 'enwiki'))
defaultSites.push('enwiki');
if (this.sitelinks.some((s) => s.site === 'ruwiki'))
defaultSites.push('ruwiki');
if (defaultSites.length > 0) {
this.modeSelectedSites.members = [...defaultSites];
this.modeSelectedSites.parentCats = [...defaultSites];
}
this.fetchLocalState();
this.status = 'idle';
} catch (e) {
console.error('CatSuggest init error:', e);
this.error =
'Sitelink məlumatları alınarkən xəta baş verdi: ' +
(e.message || e);
} finally {
this.loading = false;
}
},
fetchLocalState() {
const mode = this.mode;
const pageName = mw.config.get('wgPageName');
const qid = this.qid;
// Return existing promise if still valid for same mode/page
if (
this.localStatePromise &&
this.localStatePromise._meta === `${mode}-${pageName}`
) {
return this.localStatePromise;
}
const promise = (async () => {
if (mode === 'members') {
const cacheKey = `catsuggest-tree-${pageName}`;
const cached = mw.storage.get(cacheKey);
if (cached) {
try {
const data = JSON.parse(cached);
if (data && data.members && data.subcats) {
return {
members: new Set(data.members),
subcats: new Set(data.subcats),
};
}
} catch (e) {
mw.storage.remove(cacheKey);
}
}
const catMemResp = await api.get({
action: 'query',
list: 'categorymembers',
cmtitle: pageName,
cmtype: 'page|subcat',
cmlimit: 'max',
format: 'json',
});
const memberSet = new Set();
const subcatSet = new Set();
const depth1Subcats = [];
if (catMemResp.query && catMemResp.query.categorymembers) {
catMemResp.query.categorymembers.forEach((m) => {
memberSet.add(m.title);
if (m.ns === 14) {
subcatSet.add(m.title);
depth1Subcats.push(m.title);
}
});
}
// Depth 2
if (depth1Subcats.length > 0) {
const batchSize = 5;
for (let i = 0; i < depth1Subcats.length; i += batchSize) {
const batch = depth1Subcats.slice(i, i + batchSize);
const depth2Promises = batch.map((cat) =>
api.get({
action: 'query',
list: 'categorymembers',
cmtitle: cat,
cmtype: 'page|subcat',
cmlimit: 'max',
format: 'json',
}),
);
const depth2Results = await Promise.all(depth2Promises);
depth2Results.forEach((resp) => {
if (resp.query && resp.query.categorymembers) {
resp.query.categorymembers.forEach((m) => {
memberSet.add(m.title);
if (m.ns === 14) subcatSet.add(m.title);
});
}
});
}
}
// Cache for 3 days
mw.storage.set(
cacheKey,
JSON.stringify({
members: Array.from(memberSet),
subcats: Array.from(subcatSet),
}),
259200,
);
return { members: memberSet, subcats: subcatSet };
} else {
// mode === 'parentCats'
let currentCats = mw.config.get('wgCategories');
if (currentCats) {
return currentCats.map(
(c) => 'Kateqoriya:' + c.replaceAll('_', ' '),
);
} else {
const catResp = await api.get({
action: 'query',
titles: pageName,
prop: 'categories',
cllimit: 'max',
format: 'json',
});
const pages = catResp.query ? catResp.query.pages : {};
const page = Object.values(pages)[0];
return page && page.categories
? page.categories.map((c) => c.title)
: [];
}
}
})();
promise._meta = `${mode}-${pageName}`;
this.localStatePromise = promise;
return promise;
},
async fetchSuggestions() {
if (this.selectedSites.length === 0) return;
this.loading = true;
this.status = 'loading';
this.suggestions = [];
this.error = null;
const globalQidSet = new Set();
const qidToSites = new Map();
const fetchConcurrency = 3;
const sleep = (ms) =>
new Promise((resolve) => setTimeout(resolve, ms));
try {
const sitesToProcess = [...this.selectedSites];
// 1. Parallelize local members fetch with foreign wiki fetching
const localMembersPromise = this.fetchLocalState();
const processLanguage = async (site) => {
const siteData = this.sitelinks.find((s) => s.site === site);
if (!siteData) return;
const lang = siteData.lang;
const foreignApi = new mw.ForeignApi(
`https://${lang}.wikipedia.org/w/api.php`,
{ userAgent: 'CatSuggest/azwiki' },
);
if (this.mode === 'parentCats') {
const cacheKey = `catsuggest-cache-${this.qid}-${site}`;
const cached = mw.storage.get(cacheKey);
if (cached) {
try {
const cachedItems = JSON.parse(cached);
if (
Array.isArray(cachedItems) &&
cachedItems.length > 0 &&
typeof cachedItems[0] === 'string' &&
cachedItems[0].startsWith('Q')
) {
cachedItems.forEach((qid) => {
globalQidSet.add(qid);
if (!qidToSites.has(qid))
qidToSites.set(qid, new Set());
qidToSites.get(qid).add(lang);
});
return;
}
mw.storage.remove(cacheKey);
} catch (e) {
mw.storage.remove(cacheKey);
}
}
const genResp = await foreignApi.get({
action: 'query',
generator: 'categories',
titles: siteData.title,
gcllimit: 'max',
gclshow: '!hidden',
prop: 'pageprops',
ppprop: 'wikibase_item',
format: 'json',
});
if (!genResp.query || !genResp.query.pages) return;
const catQidsForCache = [];
Object.values(genResp.query.pages).forEach((p) => {
if (p.pageprops && p.pageprops.wikibase_item) {
const qid = p.pageprops.wikibase_item;
globalQidSet.add(qid);
catQidsForCache.push(qid);
if (!qidToSites.has(qid)) qidToSites.set(qid, new Set());
qidToSites.get(qid).add(lang);
}
});
if (catQidsForCache.length > 0) {
mw.storage.set(
cacheKey,
JSON.stringify(catQidsForCache),
300,
);
}
} else {
// Mode: members (Optimized with generator)
const genResp = await foreignApi.get({
action: 'query',
generator: 'categorymembers',
gcmtitle: siteData.title,
gcmlimit: 'max',
gcmtype: 'page|subcat',
prop: 'pageprops',
ppprop: 'wikibase_item',
format: 'json',
});
if (!genResp.query || !genResp.query.pages) return;
Object.values(genResp.query.pages).forEach((p) => {
if (p.pageprops && p.pageprops.wikibase_item) {
const qid = p.pageprops.wikibase_item;
globalQidSet.add(qid);
if (!qidToSites.has(qid)) qidToSites.set(qid, new Set());
qidToSites.get(qid).add(lang);
}
});
}
};
for (let i = 0; i < sitesToProcess.length; i += fetchConcurrency) {
const batch = sitesToProcess.slice(i, i + fetchConcurrency);
await Promise.all(batch.map((site) => processLanguage(site)));
if (i + fetchConcurrency < sitesToProcess.length)
await sleep(500);
}
const localData = await localMembersPromise;
if (globalQidSet.size === 0) {
this.error =
this.mode === 'parentCats'
? 'Seçilmiş dillərdə kateqoriya tapılmadı.'
: 'Seçilmiş dillərdə bu kateqoriyanın üzvü tapılmadı.';
this.status = 'idle';
return;
}
if (this.mode === 'parentCats') {
const excludedQids = new Set([
'Q6697530', // Kateqoriya:İnsanlar
'Q9507857', // Kateqoriya:Kişi
'Q1410641', // Kateqoriya:Kişilər'
'Q1410688', // Kateqoriya:Qadın
'Q7473085', // Kateqoriya:Qadınlar
'Q7098184', // Kateqoriya:SSRİ
'Q7031845', // Kateqoriya:Türklər
'Q2944440', // Kateqoriya:Vikipediya:Qaralama halında olan məqalələr
]);
excludedQids.forEach((qid) => globalQidSet.delete(qid));
}
const allQids = Array.from(globalQidSet);
const batchSize = 50;
const resolvePromises = [];
for (let i = 0; i < allQids.length; i += batchSize) {
const batch = allQids.slice(i, i + batchSize);
resolvePromises.push(
wikidataApi.get({
action: 'wbgetentities',
ids: batch.join('|'),
props: 'sitelinks',
sitefilter: 'azwiki',
format: 'json',
}),
);
}
const resolveResults = await Promise.all(resolvePromises);
const rawResults = [];
resolveResults.forEach((wdResp) => {
if (!wdResp.entities) return;
Object.entries(wdResp.entities).forEach(([qid, ent]) => {
if (ent.sitelinks && ent.sitelinks.azwiki) {
rawResults.push({
qid: qid,
title: ent.sitelinks.azwiki.title,
sources: Array.from(qidToSites.get(qid) || []),
});
}
});
});
if (rawResults.length === 0) {
this.error = 'Azvikidə uyğun məqalə/kateqoriya tapılmadı.';
this.status = 'idle';
return;
}
if (this.mode === 'parentCats') {
let currentCats = mw.config.get('wgCategories');
if (currentCats) {
currentCats = currentCats.map(
(c) => 'Kateqoriya:' + c.replaceAll('_', ' '),
);
} else {
const catResp = await api.get({
action: 'query',
titles: mw.config.get('wgPageName'),
prop: 'categories',
cllimit: 'max',
format: 'json',
});
const pages = catResp.query ? catResp.query.pages : {};
const page = Object.values(pages)[0];
currentCats =
page && page.categories
? page.categories.map((c) => c.title)
: [];
}
const suggestionTitles = rawResults.map((r) => r.title);
const allTitlesToCheck = [
...new Set([...suggestionTitles, ...currentCats]),
];
const allParentsSet = new Set();
const batchPromises = [];
for (let i = 0; i < allTitlesToCheck.length; i += 50) {
const batch = allTitlesToCheck.slice(i, i + 50);
batchPromises.push(
api.get({
action: 'query',
titles: batch.join('|'),
prop: 'categories',
cllimit: 'max',
format: 'json',
}),
);
}
const batchResults = await Promise.all(batchPromises);
batchResults.forEach((parentResp) => {
if (parentResp.query && parentResp.query.pages) {
Object.values(parentResp.query.pages).forEach((p) => {
if (p.categories)
p.categories.forEach((c) => allParentsSet.add(c.title));
});
}
});
const seenTitles = new Set();
this.suggestions = rawResults
.filter((r) => {
const isRedundant = allParentsSet.has(r.title);
const alreadyOnPage = currentCats.includes(r.title);
if (
!alreadyOnPage &&
!isRedundant &&
!seenTitles.has(r.title)
) {
seenTitles.add(r.title);
return true;
}
return false;
})
.map((r) => ({
title: r.title,
selected: true,
sources: r.sources,
}));
} else {
// Mode: members
const { members: currentMembersSet, subcats: currentSubcatsSet } =
localData;
const suggestionTitles = rawResults.map((r) => r.title);
const allPagesCats = new Map();
const catFetchPromises = [];
for (let i = 0; i < suggestionTitles.length; i += 50) {
const batch = suggestionTitles.slice(i, i + 50);
catFetchPromises.push(
api.get({
action: 'query',
titles: batch.join('|'),
prop: 'categories',
cllimit: 'max',
format: 'json',
}),
);
}
const catFetchResults = await Promise.all(catFetchPromises);
catFetchResults.forEach((catsResp) => {
if (catsResp.query && catsResp.query.pages) {
Object.values(catsResp.query.pages).forEach((p) => {
if (p.categories) {
allPagesCats.set(
p.title,
p.categories.map((c) => c.title),
);
}
});
}
});
const seenTitles = new Set();
this.suggestions = rawResults
.filter((item) => {
if (
currentMembersSet.has(item.title) ||
seenTitles.has(item.title)
)
return false;
seenTitles.add(item.title);
const itemCats = allPagesCats.get(item.title) || [];
const isRedundant = itemCats.some((c) =>
currentSubcatsSet.has(c),
);
return !isRedundant;
})
.map((item) => ({
title: item.title,
selected: true,
sources: item.sources,
isCategory: item.title.startsWith('Kateqoriya:'),
}));
}
this.suggestions.sort((a, b) =>
a.title
.replace(/^(Kateqoriya|Category):/i, '')
.localeCompare(b.title.replace(/^(Kateqoriya|Category):/i, '')),
);
if (this.suggestions.length === 0)
this.error = 'Yeni təklif tapılmadı.';
// Silent optimization: unselect sites that yielded 0 new suggestions in current mode
const activeLangs = new Set();
this.suggestions.forEach((s) =>
s.sources.forEach((l) => activeLangs.add(l)),
);
this.modeSelectedSites[this.mode] = this.modeSelectedSites[
this.mode
].filter((site) => {
const sl = this.sitelinks.find((link) => link.site === site);
return sl && activeLangs.has(sl.lang);
});
this.status = 'results';
} catch (e) {
console.error('Xəta:', e);
this.error = 'Xəta baş verdi: ' + (e.message || e);
this.status = 'idle';
} finally {
this.loading = false;
}
},
async applyChanges() {
if (this.mode === 'members') {
await this.addCategoryToArticles();
} else {
await this.addParentCategories();
}
},
async addParentCategories() {
const titlesToAdd = this.suggestions
.filter((s) => s.selected)
.map((s) => s.title);
if (titlesToAdd.length === 0) return;
const pageTitle = mw.config.get('wgPageName');
this.status = 'editing';
this.loading = true;
this.totalProcessingCount = 1;
this.processedCount = 0;
this.currentProcessingTitle = pageTitle;
try {
const catText =
'\n' +
titlesToAdd
.map((t) => `[[${t.replaceAll('_', ' ')}]]`)
.join('\n');
await api.postWithEditToken({
action: 'edit',
title: pageTitle,
appendtext: catText,
summary: `[[Vikipediya:Qadcetlər/Kateqoriya təklifçisi|Kateqoriya təklifçisi]] vasitəsilə +${titlesToAdd.length} kateqoriya`,
tags: 'catsuggest',
format: 'json',
});
this.processedCount = 1;
this.status = 'success';
} catch (e) {
console.error('CatSuggest edit error:', e);
this.error =
'Kateqoriyaları əlavə edərkən xəta baş verdi: ' +
(e.message || e);
this.status = 'results';
} finally {
this.loading = false;
}
},
async addCategoryToArticles() {
const selectedItems = this.suggestions.filter((s) => s.selected);
if (selectedItems.length === 0) return;
this.status = 'editing';
this.loading = true;
this.totalProcessingCount = selectedItems.length;
this.processedCount = 0;
try {
const categoryTag = `\n[[${mw.config.get('wgPageName').replaceAll('_', ' ')}]]`;
const sleep = (ms) =>
new Promise((resolve) => setTimeout(resolve, ms));
for (const item of selectedItems) {
this.currentProcessingTitle = item.title;
try {
await api.postWithEditToken({
action: 'edit',
title: item.title,
appendtext: categoryTag,
summary: `[[Vikipediya:Qadcetlər/Kateqoriya təklifçisi|Kateqoriya təklifçisi]] vasitəsilə +[[${mw.config.get('wgPageName').replaceAll('_', ' ')}]]`,
tags: 'catsuggest',
format: 'json',
});
this.processedCount++;
await sleep(300); // Stagger edits
} catch (e) {
console.error(`Error adding category to ${item.title}:`, e);
}
}
this.status = 'success';
} catch (e) {
console.error('Batch edit error:', e);
this.error =
'Məqalələri edərkən xəta baş verdi: ' + (e.message || e);
this.status = 'results';
} finally {
this.loading = false;
}
},
getPageUrl(title) {
return mw.util.getUrl(title);
},
handleClose() {
if (this.status === 'success') {
location.reload();
} else {
this.dialogShown = false;
}
},
},
template: `
<cdx-dialog
v-model:open="dialogShown"
:title="mode === 'members' ? 'Məqalə və altkateqoriya təklifləri' : 'Kateqoriya təklifləri'"
:use-close-button="true"
close-button-label="Bağla"
@update:open="handleClose"
>
<template #default>
<!-- Main Selection UI -->
<div v-if="status === 'idle' || status === 'loading' || status === 'results'">
<div v-if="isCategoryPage" style="margin-bottom: 16px;">
<cdx-tabs v-model:active="mode">
<cdx-tab name="members" label="Məqalə və altkateqoriya" />
<cdx-tab name="parentCats" label="Kateqoriya" />
</cdx-tabs>
</div>
<p v-if="mode === 'parentCats'">Digər dillərdəki məqalələrə əlavə edilmiş kateqoriyalar əsasında təkliflər.</p>
<p v-else>Digər dillərdəki eyni kateqoriyanın üzvləri əsasında bu kateqoriyaya əlavə edilə biləcək məqalələr və altkateqoriyalar.</p>
<div v-if="sitelinks.length > 0 && (status === 'idle' || status === 'loading' || status === 'results')" style="margin-bottom: 12px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
<p style="margin: 0; font-weight: bold;">i seçin:</p>
<cdx-toggle-switch v-model="allSelected" :disabled="loading">
Bütün dilləri seç ({{ sitelinks.length }})
</cdx-toggle-switch>
</div>
<div v-if="showSearch" style="margin-bottom: 12px;">
<cdx-text-input
v-model="siteFilter"
placeholder="də axtar..."
aria-label="də axtar"
:clearable="true"
>
<template #start-icon>
<cdx-icon name="search" />
</template>
</cdx-text-input>
</div>
<div :style="gridStyle">
<div v-for="site in filteredSitelinks" :key="site.site">
<cdx-checkbox v-model="selectedSites" :inputValue="site.site" :disabled="loading">
{{ site.name }} ({{ site.lang }})
</cdx-checkbox>
</div>
</div>
<cdx-message v-if="selectedSites.length > 4" :type="selectedSites.length === sitelinks.length ? 'warning' : 'info'" style="margin-top: 10px; margin-bottom: 10px;">
{{ selectedSites.length }} dil seçilib. Axtarışın təxminən **{{ estimatedTime }} saniyə** çəkəcəyi gözlənilir.
</cdx-message>
</div>
<div v-if="status === 'loading'">
<cdx-progress-bar aria-label="Yüklənir..." />
<p style="text-align: center; margin-top: 8px;">Təkliflər axtarılır...</p>
</div>
<div v-else-if="error" style="color: var(--color-error, #bf3c2c); padding: 8px; border: 1px solid currentColor; border-radius: 2px;">
{{ error }}
</div>
<div v-if="status === 'results'">
<p v-if="suggestions.length > 0">Seçilmiş dillər əsasında aşağıdakı təkliflər təqdim olunur:</p>
<div style="margin-top: 8px;">
<template v-if="mode === 'members'">
<div v-if="suggestedPages.length > 0">
<h5 style="margin: 8px 0 4px 0; border-bottom: 1px solid var(--border-color-subtle, #eaecf0);">Məqalələr ({{ suggestedPages.length }})</h5>
<div v-for="item in suggestedPages" :key="item.title" style="margin-bottom: 4px;">
<cdx-checkbox v-model="item.selected">
<a :href="getPageUrl(item.title)" target="_blank" style="color: var(--color-progressive, #36c); text-decoration: none;" @click.stop>{{ item.title }}</a>
<span v-if="item.sources && item.sources.length" style="font-size: 0.8em; color: var(--color-subtle, #72777d); margin-left: 4px;">
[{{ item.sources.join(', ') }}]
</span>
</cdx-checkbox>
</div>
</div>
<div v-if="suggestedSubcats.length > 0">
<h5 style="margin: 12px 0 4px 0; border-bottom: 1px solid var(--border-color-subtle, #eaecf0);">Altkateqoriyalar ({{ suggestedSubcats.length }})</h5>
<div v-for="item in suggestedSubcats" :key="item.title" style="margin-bottom: 4px;">
<cdx-checkbox v-model="item.selected">
<a :href="getPageUrl(item.title)" target="_blank" style="color: var(--color-progressive, #36c); text-decoration: none;" @click.stop>{{ item.title }}</a>
<span v-if="item.sources && item.sources.length" style="font-size: 0.8em; color: var(--color-subtle, #72777d); margin-left: 4px;">
[{{ item.sources.join(', ') }}]
</span>
</cdx-checkbox>
</div>
</div>
</template>
<template v-else>
<div v-for="cat in suggestions" :key="cat.title" style="margin-bottom: 4px;">
<cdx-checkbox v-model="cat.selected">
<a :href="getPageUrl(cat.title)" target="_blank" style="color: var(--color-progressive, #36c); text-decoration: none;" @click.stop>{{ cat.title }}</a>
<span v-if="cat.sources && cat.sources.length" style="font-size: 0.8em; color: var(--color-subtle, #72777d); margin-left: 4px;">
[{{ cat.sources.join(', ') }}]
</span>
</cdx-checkbox>
</div>
</template>
</div>
</div>
</div>
<!-- Progress Screen -->
<div v-else-if="status === 'editing'" style="text-align: center; padding: 20px 0;">
<cdx-progress-indicator show-label>
{{ currentProcessingTitle }} ({{ processedCount }}/{{ totalProcessingCount }})
</cdx-progress-indicator>
</div>
<!-- Success Screen -->
<div v-else-if="status === 'success'" style="text-align: center; padding: 20px 0;">
<cdx-icon name="check-circle" action="success" size="large" />
<p style="color: var(--color-success, #14866d); font-weight: bold; font-size: 1.2em; margin-top: 12px;">Kateqoriyalar uğurla əlavə edildi!</p>
<p style="margin-top: 8px;">Dəyişiklikləri görmək üçün bu pəncərəni bağlayın.</p>
</div>
</template>
<template #footer>
<div style="display: flex; justify-content: flex-end; gap: 8px; width: 100%;">
<cdx-button size="medium" @click="handleClose">{{ status === 'success' ? 'Bağla' : 'İmtina' }}</cdx-button>
<cdx-button
v-if="status === 'idle' || status === 'results'"
size="medium"
action="progressive"
weight="normal"
:disabled="!canFetch"
@click="fetchSuggestions"
>Təklifləri al</cdx-button>
<cdx-button
v-if="status === 'results' && suggestions.length > 0"
size="medium"
action="progressive"
weight="primary"
:disabled="selectedCount === 0"
@click="applyChanges"
>Əlavə et ({{ selectedCount }})</cdx-button>
</div>
</template>
</cdx-dialog>
`,
mounted() {
// Inject button below categories section
const catLinks = document.getElementById('catlinks');
if (catLinks) {
const btnContainer = document.createElement('div');
btnContainer.id = 'catsuggest-btn-container';
btnContainer.style.marginTop = '10px';
btnContainer.style.textAlign = 'left';
catLinks.parentNode.insertBefore(btnContainer, catLinks.nextSibling);
const { createApp } = require('vue');
const btnApp = createApp({
components: { 'cdx-button': CdxButton },
template: `<cdx-button size="medium" action="progressive" @click="openDialog">${mw.config.get('wgNamespaceNumber') === 14 ? 'Təkliflər' : 'Kateqoriya təklifləri'}</cdx-button>`,
methods: {
openDialog: () => {
this.toggleDialog();
},
},
});
btnApp.mount(btnContainer);
}
},
watch: {
mode() {
// Reset state when mode changes via tabs or buttons
this.suggestions = [];
this.status = 'idle';
this.error = null;
},
siteFilter(newVal) {
if (this.siteFilterTimeout) clearTimeout(this.siteFilterTimeout);
this.siteFilterTimeout = setTimeout(() => {
this.debouncedSiteFilter = newVal;
}, 150);
},
},
components: {
'cdx-button': CdxButton,
'cdx-dialog': CdxDialog,
'cdx-progress-bar': CdxProgressBar,
'cdx-icon': CdxIcon,
'cdx-checkbox': CdxCheckbox,
'cdx-message': CdxMessage,
'cdx-text-input': CdxTextInput,
'cdx-toggle-switch': CdxToggleSwitch,
'cdx-tabs': CdxTabs,
'cdx-tab': CdxTab,
'cdx-progress-indicator': CdxProgressIndicator,
},
});
const container = document.createElement('div');
document.body.appendChild(container);
app.mount(container);
});