fix: move storeRoles() to external JS with base layout inheritance
Some checks failed
Some checks failed
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:
179
app/modules/tenancy/static/store/js/roles.js
Normal file
179
app/modules/tenancy/static/store/js/roles.js
Normal 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');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -150,189 +150,5 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_scripts %}
|
{% block extra_scripts %}
|
||||||
<script>
|
<script defer src="{{ url_for('tenancy_static', path='store/js/roles.js') }}"></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>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user