feat(reception): Add activity logging, session metadata and org-selection UI (backend and frontend)
This commit is contained in:
@@ -0,0 +1,318 @@
|
||||
import * as plugins from '../../plugins.js';
|
||||
import {
|
||||
customElement,
|
||||
DeesElement,
|
||||
property,
|
||||
html,
|
||||
cssManager,
|
||||
css,
|
||||
state,
|
||||
type TemplateResult,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import { accountDesignTokens } from './sharedstyles.js';
|
||||
import * as accountStateModule from '../../states/accountstate.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'idp-org-select-modal': OrgSelectModal;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('idp-org-select-modal')
|
||||
export class OrgSelectModal extends DeesElement {
|
||||
@state()
|
||||
accessor visible: boolean = false;
|
||||
|
||||
@state()
|
||||
accessor organizations: plugins.idpInterfaces.data.IOrganization[] = [];
|
||||
|
||||
@state()
|
||||
accessor targetPath: string = '';
|
||||
|
||||
@state()
|
||||
accessor title: string = 'Select Organization';
|
||||
|
||||
@state()
|
||||
accessor description: string = 'Choose an organization to continue.';
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
accountDesignTokens,
|
||||
css`
|
||||
:host {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:host([visible]) {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
animation: fadeIn 0.15s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.modal {
|
||||
background: #18181b;
|
||||
border: 1px solid #27272a;
|
||||
border-radius: 16px;
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
animation: slideIn 0.2s ease;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid #27272a;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 4px 0;
|
||||
color: #fafafa;
|
||||
}
|
||||
|
||||
.modal-description {
|
||||
font-size: 14px;
|
||||
color: #71717a;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.org-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.org-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 14px 24px;
|
||||
border-bottom: 1px solid #27272a;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.org-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.org-item:hover {
|
||||
background: #27272a;
|
||||
}
|
||||
|
||||
.org-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 10px;
|
||||
background: #27272a;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.org-item:hover .org-icon {
|
||||
background: #3f3f46;
|
||||
}
|
||||
|
||||
.org-icon dees-icon {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.org-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.org-name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 2px;
|
||||
color: #fafafa;
|
||||
}
|
||||
|
||||
.org-slug {
|
||||
font-size: 12px;
|
||||
color: #71717a;
|
||||
}
|
||||
|
||||
.org-arrow {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 40px 24px;
|
||||
color: #71717a;
|
||||
}
|
||||
|
||||
.empty-state dees-icon {
|
||||
font-size: 40px;
|
||||
opacity: 0.5;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
padding: 16px 24px;
|
||||
border-top: 1px solid #27272a;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
if (!this.visible) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="overlay" @click=${this.handleOverlayClick}>
|
||||
<div class="modal" @click=${(e: Event) => e.stopPropagation()}>
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">${this.title}</h2>
|
||||
<p class="modal-description">${this.description}</p>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
${this.organizations.length === 0
|
||||
? this.renderEmptyState()
|
||||
: this.renderOrgList()}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<dees-button type="secondary" @clicked=${this.handleCancel}>
|
||||
Cancel
|
||||
</dees-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderOrgList(): TemplateResult {
|
||||
return html`
|
||||
<div class="org-list">
|
||||
${this.organizations.map((org) => html`
|
||||
<div class="org-item" @click=${() => this.handleSelectOrg(org)}>
|
||||
<div class="org-icon">
|
||||
<dees-icon .icon=${'lucide:building2'}></dees-icon>
|
||||
</div>
|
||||
<div class="org-info">
|
||||
<div class="org-name">${org.data.name}</div>
|
||||
<div class="org-slug">${org.data.slug}</div>
|
||||
</div>
|
||||
<dees-icon class="org-arrow" .icon=${'lucide:chevron-right'}></dees-icon>
|
||||
</div>
|
||||
`)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderEmptyState(): TemplateResult {
|
||||
return html`
|
||||
<div class="empty-state">
|
||||
<dees-icon .icon=${'lucide:building2'}></dees-icon>
|
||||
<p>You don't have any organizations yet.</p>
|
||||
<dees-button @clicked=${this.handleCreateOrg}>
|
||||
<dees-icon .icon=${'lucide:plus'} slot="iconLeft"></dees-icon>
|
||||
Create Organization
|
||||
</dees-button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
public show(options: {
|
||||
targetPath: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
}) {
|
||||
this.targetPath = options.targetPath;
|
||||
this.title = options.title || 'Select Organization';
|
||||
this.description = options.description || 'Choose an organization to continue.';
|
||||
|
||||
// Load organizations from state
|
||||
const state = accountStateModule.accountState.getState();
|
||||
this.organizations = state.organizations;
|
||||
|
||||
this.visible = true;
|
||||
this.setAttribute('visible', '');
|
||||
}
|
||||
|
||||
public hide() {
|
||||
this.visible = false;
|
||||
this.removeAttribute('visible');
|
||||
}
|
||||
|
||||
private handleOverlayClick(e: Event) {
|
||||
if ((e.target as HTMLElement).classList.contains('overlay')) {
|
||||
this.hide();
|
||||
}
|
||||
}
|
||||
|
||||
private handleCancel() {
|
||||
this.hide();
|
||||
}
|
||||
|
||||
private handleSelectOrg(org: plugins.idpInterfaces.data.IOrganization) {
|
||||
accountStateModule.accountState.dispatchAction(accountStateModule.setSelectedOrg, org);
|
||||
|
||||
// Replace :orgName placeholder with actual slug
|
||||
const path = this.targetPath.replace(':orgName', org.data.slug);
|
||||
|
||||
this.dispatchEvent(new CustomEvent('org-selected', {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
detail: { org, path },
|
||||
}));
|
||||
|
||||
this.hide();
|
||||
}
|
||||
|
||||
private handleCreateOrg() {
|
||||
this.hide();
|
||||
this.dispatchEvent(new CustomEvent('open-create-org-modal', {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user