first push message
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
/** @odoo-module **/
|
||||
// ^^^ MUST be first line, no comments/spaces before
|
||||
|
||||
import { Component, useState, onWillStart } from "@odoo/owl";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { jsonrpc } from "@web/core/network/rpc_service";
|
||||
import { useNotification } from "@web/core/notifications/notification_hook";
|
||||
|
||||
export class AppSelector extends Component {
|
||||
static template = "custom_subscriptions.AppSelectorTemplate";
|
||||
|
||||
setup() {
|
||||
this.orm = useService("orm");
|
||||
this.notification = useNotification();
|
||||
|
||||
this.state = useState({
|
||||
categories: {},
|
||||
selectedIds: [],
|
||||
loading: true
|
||||
});
|
||||
|
||||
onWillStart(async () => {
|
||||
await this.fetchApps();
|
||||
});
|
||||
}
|
||||
|
||||
async fetchApps() {
|
||||
try {
|
||||
const result = await jsonrpc("/custom_subscriptions/get_available_apps");
|
||||
this.state.categories = result;
|
||||
this.state.loading = false;
|
||||
} catch (error) {
|
||||
console.error("Failed to load apps:", error);
|
||||
this.notification.add("Error loading apps", { type: "danger" });
|
||||
}
|
||||
}
|
||||
|
||||
toggleSelection(id) {
|
||||
const idx = this.state.selectedIds.indexOf(id);
|
||||
if (idx === -1) {
|
||||
this.state.selectedIds.push(id);
|
||||
} else {
|
||||
this.state.selectedIds.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
|
||||
isSelected(id) {
|
||||
return this.state.selectedIds.includes(id);
|
||||
}
|
||||
|
||||
async installApps() {
|
||||
if (this.state.selectedIds.length === 0) {
|
||||
this.notification.add("Please select at least one app.", { type: "warning" });
|
||||
return;
|
||||
}
|
||||
if (!confirm(`Install ${this.state.selectedIds.length} app(s)?`)) return;
|
||||
|
||||
try {
|
||||
await this.orm.call("ir.module.module", "button_immediate_install", [this.state.selectedIds]);
|
||||
this.notification.add("Installing... Reloading soon", { type: "success" });
|
||||
setTimeout(() => window.location.reload(), 2000);
|
||||
} catch (error) {
|
||||
this.notification.add(error.data?.message || "Install failed", { type: "danger" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ⚠️ CRITICAL: This key MUST match the <field name="tag"> in XML exactly
|
||||
registry.category("actions").add("custom_app_selector", AppSelector);
|
||||
@@ -0,0 +1,193 @@
|
||||
.app-selector-wrapper {
|
||||
background-color: #f8f9fa;
|
||||
min-height: 100vh;
|
||||
padding-bottom: 40px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
}
|
||||
|
||||
.app-selector-header {
|
||||
text-align: center;
|
||||
padding: 30px 20px;
|
||||
background: white;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 1px solid #eee;
|
||||
|
||||
h1 {
|
||||
font-size: 2.2rem;
|
||||
margin: 0 0 8px 0;
|
||||
color: #212529;
|
||||
}
|
||||
p {
|
||||
color: #717b84;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.app-selector-content {
|
||||
display: flex;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
gap: 30px;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.app-grid {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.category-title {
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
font-size: 1.3rem;
|
||||
margin: 25px 0 15px 0;
|
||||
color: #000;
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.app-cards-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.app-card {
|
||||
background: white;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 6px;
|
||||
padding: 16px 12px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
|
||||
&:hover {
|
||||
border-color: #adb5bd;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
&.selected {
|
||||
border: 2px solid #714B67;
|
||||
background-color: #f9f5fa;
|
||||
}
|
||||
}
|
||||
|
||||
.card-check {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: -8px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
background: #714B67;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 11px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.app-card.selected .card-check {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.app-icon {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
margin-bottom: 10px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.app-name {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 4px;
|
||||
color: #212529;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.app-desc {
|
||||
font-size: 0.75rem;
|
||||
color: #717b84;
|
||||
line-height: 1.3;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
max-height: 2.6em;
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
.app-sidebar {
|
||||
width: 280px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sidebar-card {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
|
||||
position: sticky;
|
||||
top: 20px;
|
||||
}
|
||||
|
||||
.sidebar-card h3 {
|
||||
margin: 0 0 15px 0;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #eee;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.trial-info {
|
||||
background-color: #e6f4f9;
|
||||
padding: 14px;
|
||||
border-radius: 6px;
|
||||
margin: 15px 0;
|
||||
font-size: 0.85rem;
|
||||
color: #0c5460;
|
||||
line-height: 1.5;
|
||||
|
||||
p { margin: 4px 0; }
|
||||
}
|
||||
|
||||
.btn-continue {
|
||||
width: 100%;
|
||||
background-color: #714B67;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 16px;
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: #5a3c53;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background-color: #adb5bd;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 992px) {
|
||||
.app-selector-content {
|
||||
flex-direction: column;
|
||||
}
|
||||
.app-sidebar {
|
||||
width: 100%;
|
||||
}
|
||||
.sidebar-card {
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="custom_subscriptions.AppSelectorTemplate" owl="1">
|
||||
<div class="app-selector-wrapper">
|
||||
|
||||
<div class="app-selector-header">
|
||||
<h1>Choose your Apps</h1>
|
||||
<p>Free instant access. No credit card required.</p>
|
||||
</div>
|
||||
|
||||
<div class="app-selector-content">
|
||||
|
||||
<!-- App Grid -->
|
||||
<div class="app-grid">
|
||||
<t t-if="state.loading">
|
||||
<div class="text-center p-5 text-muted">Loading apps...</div>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-foreach="Object.keys(state.categories)" t-as="category" t-key="category">
|
||||
<div class="category-section">
|
||||
<h2 class="category-title" t-esc="category"/>
|
||||
<div class="app-cards-row">
|
||||
<t t-foreach="state.categories[category]" t-as="app" t-key="app.id">
|
||||
<div class="app-card"
|
||||
t-att-class="isSelected(app.id) ? 'selected' : ''"
|
||||
t-on-click="() => this.toggleSelection(app.id)">
|
||||
|
||||
<div class="card-check">
|
||||
<i class="fa fa-check"/>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<img t-att-src="app.icon" class="app-icon" alt="" onerror="this.src='/web/static/img/placeholder.png'"/>
|
||||
<div class="app-name" t-esc="app.name"/>
|
||||
<div class="app-desc" t-esc="app.shortdesc"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div class="app-sidebar">
|
||||
<div class="sidebar-card">
|
||||
<h3><t t-esc="state.selectedIds.length"/> Apps selected</h3>
|
||||
|
||||
<div class="trial-info">
|
||||
<p><strong>Community Edition</strong></p>
|
||||
<p>Install modules instantly.</p>
|
||||
<p>No credit card required.</p>
|
||||
</div>
|
||||
|
||||
<button class="btn-continue" t-on-click="installApps" t-att-disabled="state.selectedIds.length === 0">
|
||||
Continue
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
Reference in New Issue
Block a user