456 lines
11 KiB
TypeScript
456 lines
11 KiB
TypeScript
|
|
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';
|
||
|
|
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;
|
||
|
|
|
||
|
|
@state()
|
||
|
|
accessor orgName: string = '';
|
||
|
|
|
||
|
|
@state()
|
||
|
|
accessor orgSlug: string = '';
|
||
|
|
|
||
|
|
@state()
|
||
|
|
accessor validating: boolean = false;
|
||
|
|
|
||
|
|
@state()
|
||
|
|
accessor validationResult: { available: boolean; message: string } | null = null;
|
||
|
|
|
||
|
|
@state()
|
||
|
|
accessor creating: boolean = false;
|
||
|
|
|
||
|
|
@state()
|
||
|
|
accessor error: string = '';
|
||
|
|
|
||
|
|
private validationDebounceTimer: any = 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;
|
||
|
|
border-radius: 8px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.slug-label {
|
||
|
|
font-size: 11px;
|
||
|
|
font-weight: 500;
|
||
|
|
text-transform: uppercase;
|
||
|
|
letter-spacing: 0.05em;
|
||
|
|
color: #71717a;
|
||
|
|
margin-bottom: 4px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.slug-value {
|
||
|
|
font-family: 'Geist Mono', monospace;
|
||
|
|
font-size: 14px;
|
||
|
|
color: #fafafa;
|
||
|
|
}
|
||
|
|
|
||
|
|
.validation-status {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 8px;
|
||
|
|
margin-top: 12px;
|
||
|
|
padding: 10px 14px;
|
||
|
|
border-radius: 8px;
|
||
|
|
font-size: 13px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.validation-status.validating {
|
||
|
|
background: rgba(59, 130, 246, 0.1);
|
||
|
|
color: #3b82f6;
|
||
|
|
}
|
||
|
|
|
||
|
|
.validation-status.available {
|
||
|
|
background: rgba(34, 197, 94, 0.1);
|
||
|
|
color: #22c55e;
|
||
|
|
}
|
||
|
|
|
||
|
|
.validation-status.unavailable {
|
||
|
|
background: rgba(239, 68, 68, 0.1);
|
||
|
|
color: #ef4444;
|
||
|
|
}
|
||
|
|
|
||
|
|
.validation-status dees-icon {
|
||
|
|
font-size: 16px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.error-message {
|
||
|
|
margin-top: 16px;
|
||
|
|
padding: 12px 16px;
|
||
|
|
background: rgba(239, 68, 68, 0.1);
|
||
|
|
border: 1px solid rgba(239, 68, 68, 0.3);
|
||
|
|
border-radius: 8px;
|
||
|
|
color: #ef4444;
|
||
|
|
font-size: 13px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.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``;
|
||
|
|
}
|
||
|
|
|
||
|
|
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>
|
||
|
|
|
||
|
|
${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>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
`;
|
||
|
|
}
|
||
|
|
|
||
|
|
private renderValidationStatus(): TemplateResult | null {
|
||
|
|
if (!this.orgSlug) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (this.validating) {
|
||
|
|
return html`
|
||
|
|
<div class="validation-status validating">
|
||
|
|
<dees-icon .icon=${'lucide:loader-2'}></dees-icon>
|
||
|
|
Checking availability...
|
||
|
|
</div>
|
||
|
|
`;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (this.validationResult) {
|
||
|
|
if (this.validationResult.available) {
|
||
|
|
return html`
|
||
|
|
<div class="validation-status available">
|
||
|
|
<dees-icon .icon=${'lucide:check-circle'}></dees-icon>
|
||
|
|
${this.validationResult.message}
|
||
|
|
</div>
|
||
|
|
`;
|
||
|
|
} else {
|
||
|
|
return html`
|
||
|
|
<div class="validation-status unavailable">
|
||
|
|
<dees-icon .icon=${'lucide:x-circle'}></dees-icon>
|
||
|
|
${this.validationResult.message}
|
||
|
|
</div>
|
||
|
|
`;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
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);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
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;
|
||
|
|
this.orgSlug = this.generateSlug(this.orgName);
|
||
|
|
this.error = '';
|
||
|
|
|
||
|
|
// Debounce validation
|
||
|
|
if (this.validationDebounceTimer) {
|
||
|
|
clearTimeout(this.validationDebounceTimer);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (this.orgSlug) {
|
||
|
|
this.validating = true;
|
||
|
|
this.validationResult = null;
|
||
|
|
this.validationDebounceTimer = setTimeout(() => {
|
||
|
|
this.validateSlug();
|
||
|
|
}, 500);
|
||
|
|
} else {
|
||
|
|
this.validating = false;
|
||
|
|
this.validationResult = null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private generateSlug(name: string): string {
|
||
|
|
return name
|
||
|
|
.replace(/[^a-zA-Z0-9\s-]/g, '')
|
||
|
|
.replace(/\s+/g, '-')
|
||
|
|
.toLowerCase();
|
||
|
|
}
|
||
|
|
|
||
|
|
private async validateSlug() {
|
||
|
|
if (!this.orgSlug) {
|
||
|
|
this.validating = false;
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
const idpState = await IdpState.getSingletonInstance();
|
||
|
|
const result = await idpState.idpClient.createOrganization(
|
||
|
|
this.orgName,
|
||
|
|
this.orgSlug,
|
||
|
|
'checkAvailability'
|
||
|
|
);
|
||
|
|
|
||
|
|
this.validationResult = {
|
||
|
|
available: result.nameAvailable,
|
||
|
|
message: result.nameAvailable
|
||
|
|
? 'This name is available!'
|
||
|
|
: 'This name is already taken. Please choose another.',
|
||
|
|
};
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Validation error:', error);
|
||
|
|
this.validationResult = {
|
||
|
|
available: false,
|
||
|
|
message: 'Unable to validate. Please try again.',
|
||
|
|
};
|
||
|
|
} finally {
|
||
|
|
this.validating = false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private async handleCreate() {
|
||
|
|
if (!this.validationResult?.available || this.creating) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
this.creating = true;
|
||
|
|
this.error = '';
|
||
|
|
|
||
|
|
try {
|
||
|
|
const idpState = await IdpState.getSingletonInstance();
|
||
|
|
const result = await idpState.idpClient.createOrganization(
|
||
|
|
this.orgName,
|
||
|
|
this.orgSlug,
|
||
|
|
'manifest'
|
||
|
|
);
|
||
|
|
|
||
|
|
// Update state with new organization
|
||
|
|
const currentState = accountStateModule.accountState.getState();
|
||
|
|
currentState.organizations.push(result.resultingOrganization);
|
||
|
|
accountStateModule.accountState.dispatchAction(
|
||
|
|
accountStateModule.setSelectedOrg,
|
||
|
|
result.resultingOrganization
|
||
|
|
);
|
||
|
|
|
||
|
|
this.dispatchEvent(new CustomEvent('org-created', {
|
||
|
|
bubbles: true,
|
||
|
|
composed: true,
|
||
|
|
detail: { org: result.resultingOrganization },
|
||
|
|
}));
|
||
|
|
|
||
|
|
this.hide();
|
||
|
|
} 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;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|