const PAGE_SIZE_DESKTOP = 6; const PAGE_SIZE_MOBILE = 4; const MOBILE_BREAKPOINT = 960; function getSortOptions() { return [ { value: 'created_desc', label: (window.t && t('sort.created_desc')) || 'Sort: Last created' }, { value: 'created_asc', label: (window.t && t('sort.created_asc')) || 'Sort: Oldest first' }, { value: 'name_asc', label: (window.t && t('sort.name_asc')) || 'Sort: Name A–Z' }, { value: 'name_desc', label: (window.t && t('sort.name_desc')) || 'Sort: Name Z–A' }, ]; } function getTabDefs() { return [ { id: 'all', label: (window.t && t('tab.all')) || 'All' }, { id: 'active', label: (window.t && t('tab.active')) || 'Active' }, { id: 'draft', label: (window.t && t('tab.draft')) || 'Draft' }, { id: 'error', label: (window.t && t('tab.error')) || 'Error' }, { id: 'inactive', label: (window.t && t('tab.inactive')) || 'Inactive' }, ]; } document.addEventListener('alpine:init', () => { Alpine.data('catalog', () => ({ // ── raw data ── operations: [], agents: [], loading: true, // ── page size (responsive) ── pageSize: PAGE_SIZE_DESKTOP, // ── filter state ── search: '', tab: 'all', filterProtocol: null, filterCategory: null, filterAgent: null, sort: 'created_desc', page: 1, // ── dropdown open state ── openDropdown: null, // ── mobile nav ── navOpen: false, // ── derived option lists (built after data loads) ── categoryOptions: [], async init() { const [opsRes, agentsRes] = await Promise.all([ fetch((window.DATA_URL||'data/')+'operations.json'), fetch((window.DATA_URL||'data/')+'agents.json'), ]); let ops = await opsRes.json(); this.agents = await agentsRes.json(); // Apply localStorage overrides (from wizard create/edit) try { const overrides = JSON.parse(localStorage.getItem('crank_ops_overrides') || '[]'); if (overrides.length) { const existingIds = new Set(ops.map(o => o.id)); ops = ops.map(op => { const ov = overrides.find(o => o.id === op.id); return ov ? Object.assign({}, op, ov) : op; }); overrides .filter(o => !existingIds.has(o.id)) .forEach(o => ops.push(o)); } } catch (e) {} this.operations = ops; this.categoryOptions = [...new Set(this.operations.map(op => op.category))].filter(Boolean).sort(); this.loading = false; // Responsive page size const updatePageSize = () => { this.pageSize = window.innerWidth <= MOBILE_BREAKPOINT ? PAGE_SIZE_MOBILE : PAGE_SIZE_DESKTOP; this.page = 1; }; updatePageSize(); window.addEventListener('resize', updatePageSize); // Re-render labels on language change window.addEventListener('crank:langchange', () => { this.sort = this.sort; applyLang(); }); // Close dropdowns and mobile nav on outside click document.addEventListener('click', (e) => { if (!e.target.closest('.filter-dropdown, .sort-dropdown, .user-menu')) { this.openDropdown = null; } if (!e.target.closest('.navbar')) { this.navOpen = false; } }); }, // ── helpers for filtering (shared between filtered getter and tabCount) ── _applyNonStatusFilters(ops) { if (this.search.trim()) { const q = this.search.toLowerCase(); ops = ops.filter(op => op.name.toLowerCase().includes(q) || op.display_name.toLowerCase().includes(q) || (op.target_url || '').toLowerCase().includes(q) ); } if (this.filterProtocol) { ops = ops.filter(op => op.protocol === this.filterProtocol); } if (this.filterCategory) { ops = ops.filter(op => op.category === this.filterCategory); } if (this.filterAgent) { const agent = this.agents.find(a => a.id === this.filterAgent); const ids = agent ? agent.operation_ids : []; ops = ops.filter(op => ids.includes(op.id)); } return ops; }, // ── computed: filtered + sorted + paginated ── get filtered() { let ops = this._applyNonStatusFilters(this.operations); if (this.tab !== 'all') { ops = ops.filter(op => op.status === this.tab); } ops = [...ops].sort((a, b) => { switch (this.sort) { case 'created_desc': return new Date(b.created_at) - new Date(a.created_at); case 'created_asc': return new Date(a.created_at) - new Date(b.created_at); case 'name_asc': return a.name.localeCompare(b.name); case 'name_desc': return b.name.localeCompare(a.name); default: return 0; } }); return ops; }, get totalFiltered() { return this.filtered.length; }, get totalPages() { return Math.max(1, Math.ceil(this.totalFiltered / this.pageSize)); }, get paginated() { const start = (this.page - 1) * this.pageSize; return this.filtered.slice(start, start + this.pageSize); }, get pageStart() { return this.totalFiltered === 0 ? 0 : (this.page - 1) * this.pageSize + 1; }, get pageEnd() { return Math.min(this.page * this.pageSize, this.totalFiltered); }, // ── tab counts (respect non-status filters so numbers are consistent) ── tabCount(tab) { const ops = this._applyNonStatusFilters(this.operations); if (tab === 'all') return ops.length; return ops.filter(op => op.status === tab).length; }, // ── active filter chips (for dismissible chip row) ── get activeChips() { const chips = []; if (this.filterProtocol) { chips.push({ key: 'protocol', label: 'Protocol: ' + this.filterProtocol.toUpperCase() }); } if (this.filterCategory) { chips.push({ key: 'category', label: 'Category: ' + this.filterCategory }); } if (this.filterAgent) { const a = this.agents.find(ag => ag.id === this.filterAgent); chips.push({ key: 'agent', label: 'Agent: ' + (a ? a.display_name : '') }); } return chips; }, get hasActiveFilters() { return !!(this.filterProtocol || this.filterCategory || this.filterAgent || this.search.trim()); }, clearChip(key) { if (key === 'protocol') this.filterProtocol = null; if (key === 'category') this.filterCategory = null; if (key === 'agent') this.filterAgent = null; this.page = 1; }, clearAllFilters() { this.filterProtocol = null; this.filterCategory = null; this.filterAgent = null; this.search = ''; this.tab = 'all'; this.page = 1; }, // ── agent lookup per operation (for badges in rows) ── get agentsByOpId() { const map = {}; this.agents.forEach(a => { a.operation_ids.forEach(id => { if (!map[id]) map[id] = []; map[id].push(a); }); }); return map; }, // ── active agents only (for filter dropdown) ── get activeAgents() { return this.agents.filter(a => a.status === 'active'); }, // ── sort label ── get sortLabel() { return getSortOptions().find(o => o.value === this.sort)?.label ?? 'Sort'; }, // ── actions ── setTab(tab) { this.tab = tab; this.page = 1; }, setSearch(val) { this.search = val; this.page = 1; }, setProtocol(val) { this.filterProtocol = this.filterProtocol === val ? null : val; this.openDropdown = null; this.page = 1; }, setCategory(val) { this.filterCategory = this.filterCategory === val ? null : val; this.openDropdown = null; this.page = 1; }, setSort(val) { this.sort = val; this.openDropdown = null; }, setAgent(val) { this.filterAgent = this.filterAgent === val ? null : val; this.openDropdown = null; this.page = 1; }, clearProtocol() { this.filterProtocol = null; this.openDropdown = null; this.page = 1; }, clearCategory() { this.filterCategory = null; this.openDropdown = null; this.page = 1; }, clearAgent() { this.filterAgent = null; this.openDropdown = null; this.page = 1; }, toggleDropdown(name) { this.openDropdown = this.openDropdown === name ? null : name; }, goPage(p) { if (p >= 1 && p <= this.totalPages) this.page = p; }, // ── operation CRUD ── editOperation(op) { try { sessionStorage.setItem('wizard_edit', JSON.stringify(op)); } catch (e) {} window.location.href = (window.APP_BASE||'')+'html/wizard/?mode=edit'; }, deleteOperation(id) { if (!confirm('Delete this operation? This cannot be undone.')) return; this.operations = this.operations.filter(op => op.id !== id); // Persist deletion in overrides as a tombstone try { const overrides = JSON.parse(localStorage.getItem('crank_ops_overrides') || '[]'); const filtered = overrides.filter(o => o.id !== id); filtered.push({ id, _deleted: true }); localStorage.setItem('crank_ops_overrides', JSON.stringify(filtered)); } catch (e) {} }, // ── helpers для шаблона ── protocolLabel(op) { if (op.protocol === 'rest') return `REST · ${op.method}`; if (op.protocol === 'graphql') return 'GraphQL'; return 'gRPC'; }, protocolClass(op) { return `badge badge-${op.protocol}`; }, statusClass(op) { return `status-badge status-${op.status}`; }, statusLabel(op) { return op.status.charAt(0).toUpperCase() + op.status.slice(1); }, formatDate(dateStr) { if (!dateStr) return '—'; return new Date(dateStr).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); }, truncateUrl(url) { if (!url) return ''; return url.length > 42 ? url.slice(0, 42) + '…' : url; }, get sortOptions() { return getSortOptions(); }, get tabDefs() { return getTabDefs(); }, handleNewOperation() { sessionStorage.removeItem('wizard_edit'); window.location.href = (window.APP_BASE||'')+'html/wizard/'; }, handleLogout() { localStorage.removeItem('crank_user'); window.location.href = (window.APP_BASE||'')+'html/login.html'; }, })); });