fix: move storeRoles() to external JS with base layout inheritance
Some checks failed
CI / ruff (push) Successful in 10s
CI / dependency-scanning (push) Has been cancelled
CI / docs (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / pytest (push) Has started running
CI / validate (push) Has been cancelled

The inline storeRoles() was missing ...data() spread, causing Alpine
errors for dark mode, sidebar, storeCode etc. Follow the same pattern
as team.js: external JS file with ...data() and parent init() call.
Uses apiClient and Utils.showToast per architecture rules.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-26 19:15:19 +01:00
parent cb3bc3c118
commit c82210795f
2 changed files with 180 additions and 185 deletions

View File

@@ -0,0 +1,179 @@
/**
* Store Roles Management — Alpine.js component
*
* Provides CRUD for custom roles with a permission matrix UI.
* Loaded on /store/{store_code}/team/roles.
*/
const storeRolesLog = createModuleLogger?.('storeRoles') ?? console;
function storeRoles() {
storeRolesLog.info('storeRoles() called');
return {
// Inherit base layout state
...data(),
// Set page identifier
currentPage: 'team',
// Component state
roles: [],
loading: true,
error: false,
saving: false,
showRoleModal: false,
editingRole: null,
roleForm: { name: '', permissions: [] },
permissionsByCategory: {},
presetRoles: ['manager', 'staff', 'support', 'viewer', 'marketing'],
async init() {
// Guard against multiple initialization
if (window._storeRolesInitialized) {
storeRolesLog.warn('Already initialized, skipping');
return;
}
window._storeRolesInitialized = true;
// Call parent init to set storeCode from URL
const parentInit = data().init;
if (parentInit) {
await parentInit.call(this);
}
await this.loadPermissions();
await this.loadRoles();
},
async loadPermissions() {
try {
// Group known permissions by category prefix
const allPerms = window.USER_PERMISSIONS || [];
this.permissionsByCategory = this.groupPermissions(allPerms);
} catch (e) {
storeRolesLog.warn('Could not load permission categories:', e);
}
},
groupPermissions(permIds) {
// Known permission categories from the codebase
const knownPerms = [
'dashboard.view',
'settings.view', 'settings.edit', 'settings.theme', 'settings.domains',
'products.view', 'products.create', 'products.edit', 'products.delete', 'products.import', 'products.export',
'orders.view', 'orders.edit', 'orders.cancel', 'orders.refund',
'customers.view', 'customers.edit', 'customers.delete', 'customers.export',
'stock.view', 'stock.edit', 'stock.transfer',
'team.view', 'team.invite', 'team.edit', 'team.remove',
'analytics.view', 'analytics.export',
'messaging.view_messages', 'messaging.send_messages', 'messaging.manage_templates',
'billing.view_tiers', 'billing.manage_tiers', 'billing.view_subscriptions', 'billing.manage_subscriptions', 'billing.view_invoices',
'cms.view_pages', 'cms.manage_pages', 'cms.view_media', 'cms.manage_media', 'cms.manage_themes',
'loyalty.view_programs', 'loyalty.manage_programs', 'loyalty.view_rewards', 'loyalty.manage_rewards',
'cart.view', 'cart.manage',
];
const groups = {};
for (const perm of knownPerms) {
const cat = perm.split('.')[0];
if (!groups[cat]) groups[cat] = [];
groups[cat].push({ id: perm });
}
return groups;
},
async loadRoles() {
this.loading = true;
this.error = false;
try {
const response = await apiClient.get('/store/team/roles');
this.roles = response.roles || [];
} catch (e) {
this.error = true;
storeRolesLog.error('Error loading roles:', e);
} finally {
this.loading = false;
}
},
isPresetRole(name) {
return this.presetRoles.includes(name.toLowerCase());
},
openCreateModal() {
this.editingRole = null;
this.roleForm = { name: '', permissions: [] };
this.showRoleModal = true;
},
openEditModal(role) {
this.editingRole = role;
this.roleForm = {
name: role.name,
permissions: [...(role.permissions || [])],
};
this.showRoleModal = true;
},
togglePermission(permId) {
const idx = this.roleForm.permissions.indexOf(permId);
if (idx >= 0) {
this.roleForm.permissions.splice(idx, 1);
} else {
this.roleForm.permissions.push(permId);
}
},
toggleCategory(category) {
const perms = this.permissionsByCategory[category] || [];
const permIds = perms.map(p => p.id);
const allSelected = permIds.every(id => this.roleForm.permissions.includes(id));
if (allSelected) {
this.roleForm.permissions = this.roleForm.permissions.filter(id => !permIds.includes(id));
} else {
for (const id of permIds) {
if (!this.roleForm.permissions.includes(id)) {
this.roleForm.permissions.push(id);
}
}
}
},
isCategoryFullySelected(category) {
const perms = this.permissionsByCategory[category] || [];
return perms.length > 0 && perms.every(p => this.roleForm.permissions.includes(p.id));
},
async saveRole() {
this.saving = true;
try {
if (this.editingRole) {
await apiClient.put(`/store/team/roles/${this.editingRole.id}`, this.roleForm);
} else {
await apiClient.post('/store/team/roles', this.roleForm);
}
this.showRoleModal = false;
Utils.showToast('Role saved successfully', 'success');
await this.loadRoles();
} catch (e) {
storeRolesLog.error('Error saving role:', e);
Utils.showToast(e.message || 'Failed to save role', 'error');
} finally {
this.saving = false;
}
},
async confirmDelete(role) {
if (!confirm(`Delete role "${role.name}"? This cannot be undone.`)) return;
try {
await apiClient.delete(`/store/team/roles/${role.id}`);
Utils.showToast('Role deleted successfully', 'success');
await this.loadRoles();
} catch (e) {
storeRolesLog.error('Error deleting role:', e);
Utils.showToast(e.message || 'Failed to delete role', 'error');
}
},
};
}

View File

@@ -150,189 +150,5 @@
{% endblock %}
{% block extra_scripts %}
<script>
function storeRoles() {
return {
roles: [],
loading: true,
error: false,
saving: false,
showRoleModal: false,
editingRole: null,
roleForm: { name: '', permissions: [] },
permissionsByCategory: {},
presetRoles: ['manager', 'staff', 'support', 'viewer', 'marketing'],
async init() {
await this.loadPermissions();
await this.loadRoles();
},
async loadPermissions() {
try {
const resp = await fetch(`/api/v1/store/team/me/permissions`, {
headers: { 'Authorization': `Bearer ${this.getToken()}` }
});
// We need a permissions-by-category endpoint; for now use a simple list
// Group known permissions by category prefix
const allPerms = window.USER_PERMISSIONS || [];
this.permissionsByCategory = this.groupPermissions(allPerms);
} catch (e) {
console.warn('Could not load permission categories:', e);
}
},
groupPermissions(permIds) {
// Known permission categories from the codebase
const knownPerms = [
'dashboard.view',
'settings.view', 'settings.edit', 'settings.theme', 'settings.domains',
'products.view', 'products.create', 'products.edit', 'products.delete', 'products.import', 'products.export',
'orders.view', 'orders.edit', 'orders.cancel', 'orders.refund',
'customers.view', 'customers.edit', 'customers.delete', 'customers.export',
'stock.view', 'stock.edit', 'stock.transfer',
'team.view', 'team.invite', 'team.edit', 'team.remove',
'analytics.view', 'analytics.export',
'messaging.view_messages', 'messaging.send_messages', 'messaging.manage_templates',
'billing.view_tiers', 'billing.manage_tiers', 'billing.view_subscriptions', 'billing.manage_subscriptions', 'billing.view_invoices',
'cms.view_pages', 'cms.manage_pages', 'cms.view_media', 'cms.manage_media', 'cms.manage_themes',
'loyalty.view_programs', 'loyalty.manage_programs', 'loyalty.view_rewards', 'loyalty.manage_rewards',
'cart.view', 'cart.manage',
];
const groups = {};
for (const perm of knownPerms) {
const cat = perm.split('.')[0];
if (!groups[cat]) groups[cat] = [];
groups[cat].push({ id: perm });
}
return groups;
},
async loadRoles() {
this.loading = true;
this.error = false;
try {
const resp = await fetch(`/api/v1/store/team/roles`, {
headers: { 'Authorization': `Bearer ${this.getToken()}` }
});
if (!resp.ok) throw new Error('Failed to load roles');
const data = await resp.json();
this.roles = data.roles || [];
} catch (e) {
this.error = true;
console.error('Error loading roles:', e);
} finally {
this.loading = false;
}
},
isPresetRole(name) {
return this.presetRoles.includes(name.toLowerCase());
},
openCreateModal() {
this.editingRole = null;
this.roleForm = { name: '', permissions: [] };
this.showRoleModal = true;
},
openEditModal(role) {
this.editingRole = role;
this.roleForm = {
name: role.name,
permissions: [...(role.permissions || [])],
};
this.showRoleModal = true;
},
togglePermission(permId) {
const idx = this.roleForm.permissions.indexOf(permId);
if (idx >= 0) {
this.roleForm.permissions.splice(idx, 1);
} else {
this.roleForm.permissions.push(permId);
}
},
toggleCategory(category) {
const perms = this.permissionsByCategory[category] || [];
const permIds = perms.map(p => p.id);
const allSelected = permIds.every(id => this.roleForm.permissions.includes(id));
if (allSelected) {
this.roleForm.permissions = this.roleForm.permissions.filter(id => !permIds.includes(id));
} else {
for (const id of permIds) {
if (!this.roleForm.permissions.includes(id)) {
this.roleForm.permissions.push(id);
}
}
}
},
isCategoryFullySelected(category) {
const perms = this.permissionsByCategory[category] || [];
return perms.length > 0 && perms.every(p => this.roleForm.permissions.includes(p.id));
},
async saveRole() {
this.saving = true;
try {
const url = this.editingRole
? `/api/v1/store/team/roles/${this.editingRole.id}`
: '/api/v1/store/team/roles';
const method = this.editingRole ? 'PUT' : 'POST';
const resp = await fetch(url, {
method,
headers: {
'Authorization': `Bearer ${this.getToken()}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(this.roleForm),
});
if (!resp.ok) {
const err = await resp.json();
alert(err.detail || 'Failed to save role');
return;
}
this.showRoleModal = false;
await this.loadRoles();
} catch (e) {
console.error('Error saving role:', e);
alert('Failed to save role');
} finally {
this.saving = false;
}
},
async confirmDelete(role) {
if (!confirm(`Delete role "${role.name}"? This cannot be undone.`)) return;
try {
const resp = await fetch(`/api/v1/store/team/roles/${role.id}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${this.getToken()}` },
});
if (!resp.ok) {
const err = await resp.json();
alert(err.detail || 'Failed to delete role');
return;
}
await this.loadRoles();
} catch (e) {
console.error('Error deleting role:', e);
alert('Failed to delete role');
}
},
getToken() {
return document.cookie.split(';')
.map(c => c.trim())
.find(c => c.startsWith('store_token='))
?.split('=')[1] || '';
},
};
}
</script>
<script defer src="{{ url_for('tenancy_static', path='store/js/roles.js') }}"></script>
{% endblock %}