feat(account): Refactor account UI: migrate modals to promise-based show() API and improve navigation URL tracking
This commit is contained in:
@@ -2,10 +2,9 @@ import * as plugins from '../../plugins.js';
|
||||
import {
|
||||
customElement,
|
||||
DeesElement,
|
||||
property,
|
||||
html,
|
||||
cssManager,
|
||||
css,
|
||||
cssManager,
|
||||
state,
|
||||
type TemplateResult,
|
||||
} from '@design.estate/dees-element';
|
||||
@@ -14,17 +13,9 @@ import { accountDesignTokens } from './sharedstyles.js';
|
||||
import * as accountStateModule from '../../states/accountstate.js';
|
||||
import { IdpState } from '../../states/idp.state.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'idp-create-org-modal': CreateOrgModal;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('idp-create-org-modal')
|
||||
export class CreateOrgModal extends DeesElement {
|
||||
@state()
|
||||
accessor visible: boolean = false;
|
||||
|
||||
// Internal form element for reactive state management
|
||||
@customElement('idp-create-org-form')
|
||||
class CreateOrgForm extends DeesElement {
|
||||
@state()
|
||||
accessor orgName: string = '';
|
||||
|
||||
@@ -44,126 +35,21 @@ export class CreateOrgModal extends DeesElement {
|
||||
accessor error: string = '';
|
||||
|
||||
private validationDebounceTimer: any = null;
|
||||
public resolveWith: ((org: plugins.idpInterfaces.data.IOrganization | null) => void) | null = null;
|
||||
public modal: plugins.deesCatalog.DeesModal | null = null;
|
||||
|
||||
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: 480px;
|
||||
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: 24px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
color: #a1a1aa;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: 10px 14px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #27272a;
|
||||
background: #0a0a0a;
|
||||
color: #fafafa;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
transition: border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
outline: none;
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
|
||||
.form-input:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.slug-preview {
|
||||
margin-top: 12px;
|
||||
padding: 12px 16px;
|
||||
background: #0a0a0a;
|
||||
border: 1px solid #27272a;
|
||||
background: var(--dees-color-background);
|
||||
border: 1px solid var(--dees-color-line);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
@@ -172,14 +58,14 @@ export class CreateOrgModal extends DeesElement {
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: #71717a;
|
||||
color: var(--dees-color-muted);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.slug-value {
|
||||
font-family: 'Geist Mono', monospace;
|
||||
font-size: 14px;
|
||||
color: #fafafa;
|
||||
color: var(--dees-color-text);
|
||||
}
|
||||
|
||||
.validation-status {
|
||||
@@ -221,69 +107,36 @@ export class CreateOrgModal extends DeesElement {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
padding: 16px 24px;
|
||||
border-top: 1px solid #27272a;
|
||||
.description {
|
||||
color: var(--dees-color-muted);
|
||||
font-size: 14px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
if (!this.visible) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const canCreate = this.orgName.length > 0 &&
|
||||
this.validationResult?.available &&
|
||||
!this.validating &&
|
||||
!this.creating;
|
||||
|
||||
return html`
|
||||
<div class="overlay" @click=${this.handleOverlayClick}>
|
||||
<div class="modal" @click=${(e: Event) => e.stopPropagation()}>
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">Create Organization</h2>
|
||||
<p class="modal-description">Create a new organization to manage apps, users, and billing.</p>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Organization Name</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-input"
|
||||
placeholder="e.g., Acme Inc."
|
||||
.value=${this.orgName}
|
||||
@input=${this.handleNameInput}
|
||||
?disabled=${this.creating}
|
||||
/>
|
||||
</div>
|
||||
<div class="description">Create a new organization to manage apps, users, and billing.</div>
|
||||
<dees-input-text
|
||||
.label=${'Organization Name'}
|
||||
.placeholder=${'e.g., Acme Inc.'}
|
||||
.value=${this.orgName}
|
||||
?disabled=${this.creating}
|
||||
></dees-input-text>
|
||||
|
||||
${this.orgSlug ? html`
|
||||
<div class="slug-preview">
|
||||
<div class="slug-label">Organization URL Slug</div>
|
||||
<div class="slug-value">${this.orgSlug}</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${this.renderValidationStatus()}
|
||||
|
||||
${this.error ? html`
|
||||
<div class="error-message">${this.error}</div>
|
||||
` : ''}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<dees-button type="secondary" @clicked=${this.handleCancel} ?disabled=${this.creating}>
|
||||
Cancel
|
||||
</dees-button>
|
||||
<dees-button @clicked=${this.handleCreate} ?disabled=${!canCreate} .status=${this.creating ? 'pending' : 'normal'}>
|
||||
${this.creating ? 'Creating...' : 'Create Organization'}
|
||||
</dees-button>
|
||||
</div>
|
||||
${this.orgSlug ? html`
|
||||
<div class="slug-preview">
|
||||
<div class="slug-label">Organization URL Slug</div>
|
||||
<div class="slug-value">${this.orgSlug}</div>
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${this.renderValidationStatus()}
|
||||
|
||||
${this.error ? html`
|
||||
<div class="error-message">${this.error}</div>
|
||||
` : ''}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -322,40 +175,17 @@ export class CreateOrgModal extends DeesElement {
|
||||
return null;
|
||||
}
|
||||
|
||||
public show() {
|
||||
this.orgName = '';
|
||||
this.orgSlug = '';
|
||||
this.validating = false;
|
||||
this.validationResult = null;
|
||||
this.creating = false;
|
||||
this.error = '';
|
||||
this.visible = true;
|
||||
this.setAttribute('visible', '');
|
||||
}
|
||||
|
||||
public hide() {
|
||||
this.visible = false;
|
||||
this.removeAttribute('visible');
|
||||
if (this.validationDebounceTimer) {
|
||||
clearTimeout(this.validationDebounceTimer);
|
||||
public async firstUpdated() {
|
||||
const inputElement = this.shadowRoot.querySelector('dees-input-text') as any;
|
||||
if (inputElement) {
|
||||
inputElement.changeSubject.subscribe((element: any) => {
|
||||
this.handleNameInput(element.value);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private handleOverlayClick(e: Event) {
|
||||
if ((e.target as HTMLElement).classList.contains('overlay') && !this.creating) {
|
||||
this.hide();
|
||||
}
|
||||
}
|
||||
|
||||
private handleCancel() {
|
||||
if (!this.creating) {
|
||||
this.hide();
|
||||
}
|
||||
}
|
||||
|
||||
private handleNameInput(e: Event) {
|
||||
const input = e.target as HTMLInputElement;
|
||||
this.orgName = input.value;
|
||||
private handleNameInput(value: string) {
|
||||
this.orgName = value;
|
||||
this.orgSlug = this.generateSlug(this.orgName);
|
||||
this.error = '';
|
||||
|
||||
@@ -414,8 +244,15 @@ export class CreateOrgModal extends DeesElement {
|
||||
}
|
||||
}
|
||||
|
||||
private async handleCreate() {
|
||||
if (!this.validationResult?.available || this.creating) {
|
||||
public canCreate(): boolean {
|
||||
return this.orgName.length > 0 &&
|
||||
this.validationResult?.available === true &&
|
||||
!this.validating &&
|
||||
!this.creating;
|
||||
}
|
||||
|
||||
public async handleCreate(): Promise<void> {
|
||||
if (!this.canCreate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -438,18 +275,52 @@ export class CreateOrgModal extends DeesElement {
|
||||
result.resultingOrganization
|
||||
);
|
||||
|
||||
this.dispatchEvent(new CustomEvent('org-created', {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
detail: { org: result.resultingOrganization },
|
||||
}));
|
||||
|
||||
this.hide();
|
||||
this.modal?.destroy();
|
||||
this.resolveWith?.(result.resultingOrganization);
|
||||
} catch (error) {
|
||||
console.error('Error creating organization:', error);
|
||||
this.error = error instanceof Error ? error.message : 'Failed to create organization. Please try again.';
|
||||
} finally {
|
||||
this.creating = false;
|
||||
}
|
||||
}
|
||||
|
||||
public handleCancel(): void {
|
||||
if (this.validationDebounceTimer) {
|
||||
clearTimeout(this.validationDebounceTimer);
|
||||
}
|
||||
this.modal?.destroy();
|
||||
this.resolveWith?.(null);
|
||||
}
|
||||
}
|
||||
|
||||
// Export the modal utility class
|
||||
export class CreateOrgModal {
|
||||
public static async show(): Promise<plugins.idpInterfaces.data.IOrganization | null> {
|
||||
return new Promise<plugins.idpInterfaces.data.IOrganization | null>((resolve) => {
|
||||
const formElement = new CreateOrgForm();
|
||||
formElement.resolveWith = resolve;
|
||||
|
||||
plugins.deesCatalog.DeesModal.createAndShow({
|
||||
heading: 'Create Organization',
|
||||
content: html`${formElement}`,
|
||||
menuOptions: [
|
||||
{
|
||||
name: 'Cancel',
|
||||
action: async () => {
|
||||
formElement.handleCancel();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Create Organization',
|
||||
action: async () => {
|
||||
await formElement.handleCreate();
|
||||
},
|
||||
},
|
||||
],
|
||||
width: 480,
|
||||
}).then((modal) => {
|
||||
formElement.modal = modal;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user