Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/assets/icons/ErrorCircleIcon.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="32" height="32" rx="16" fill="#31353F" />
<path
d="M16 6C21.523 6 26 10.478 26 16C26 21.522 21.523 26 16 26C10.477 26 6 21.522 6 16C6 10.478 10.477 6 16 6ZM16.0018 19.0037C15.4503 19.0037 15.0031 19.4508 15.0031 20.0024C15.0031 20.5539 15.4503 21.001 16.0018 21.001C16.5533 21.001 17.0005 20.5539 17.0005 20.0024C17.0005 19.4508 16.5533 19.0037 16.0018 19.0037ZM15.9996 11C15.4868 11.0002 15.0643 11.3864 15.0067 11.8837L15 12.0004L15.0018 17.0012L15.0086 17.1179C15.0665 17.6152 15.4893 18.0011 16.0022 18.0009C16.515 18.0007 16.9375 17.6145 16.9951 17.1171L17.0018 17.0005L17 11.9996L16.9932 11.883C16.9353 11.3857 16.5125 10.9998 15.9996 11Z"
fill="#EB5651"
/>
</svg>
171 changes: 171 additions & 0 deletions src/components/DowngradePlan/ChooseActiveMembersModal.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
<script lang="ts">
import { createEventDispatcher, onMount } from 'svelte';
import DowngradeModalButtons from '@/ui/DowngradeModalButtons/DowngradeModalButtons.svelte';
import TableModalCommonLayout from './Common/TableModalCommonLayout.svelte';
import { hubsService } from '@/services/hubs.service';
import { userService, UserService } from '@/services/users.service';
import { createQuery } from '@/services/api.common';

export let isOpen: boolean = false;
export let planLimits:[]
export let users = [];
export let hubId: string;
export let hubOwner: any;
export let expiryDate: string;


const dispatch = createEventDispatcher();
let selected = new Set();
$: maxSelectable = planLimits?.usersPerHub?.value;
$: filteredUsers = Array.isArray(users) ? users : [];

const toggleMember = (id: string) => {
if (selected.has(id)) {
selected.delete(id);
} else if (selected.size < maxSelectable) {
selected.add(id);
}
selected = new Set(selected);
};

const getLastActive = async () => {
try {
const res = await userService.getUsers();
if (res?.data?.users?.length) {
users = users.map((u) => {
const match = res.data.users.find(
(apiUser) =>
apiUser.id === u.id ||
apiUser._id === u._id ||
apiUser.email === u.email
);

return {
...u,
lastActiveAt: match?.lastActive || u.lastActiveAt || '',
};
});
}
} catch (err) {
console.error('Error fetching last active users:', err);
}
};

const handleNext = () => {
const selectedMembers = filteredUsers
.filter((ws) => selected.has(ws.id))
.map((ws) => ({
id: ws.id,
email: ws.email,
}));
dispatch('next', { selected: selectedMembers });
};

function getFirstName(fullNameOrEmail: string): string {
if (!fullNameOrEmail) return '';
if (fullNameOrEmail.includes('@')) {
return fullNameOrEmail.split('@')[0];
}
return fullNameOrEmail.split(' ')[0];
}

function getTimeDifference(lastActiveAt: string): string {
if (!lastActiveAt) return 'Never';
const lastActiveDate = new Date(lastActiveAt);
const now = new Date();
const diffInMinutes = Math.floor((now.getTime() - lastActiveDate.getTime()) / (1000 * 60));

if (diffInMinutes < 1) return 'Just now';
if (diffInMinutes < 60) return `${diffInMinutes} minute${diffInMinutes > 1 ? 's' : ''} ago`;

const diffInHours = Math.floor(diffInMinutes / 60);
if (diffInHours < 24) return `${diffInHours} hour${diffInHours > 1 ? 's' : ''} ago`;

const diffInDays = Math.floor(diffInHours / 24);
if (diffInDays === 1) return '1 day ago';
return `${diffInDays} days ago`;
}

function getRoleLabel(role: string): string {
const r = role?.toLowerCase?.() || '';
if (r === 'owner') return 'Owner';
if (r === 'admin') return 'Admin';
return 'Member';
}

$: isConfirmEnabled = (() => {
if (!filteredUsers.length) return true;

if (filteredUsers.length <= maxSelectable) {
return selected.size >= 1; // must select at least one
}

return selected.size === maxSelectable; // must select exactly 4
})();
$: confirmButtonText = filteredUsers.length === 0 ? 'Continue' : 'Confirm Selections';
onMount(()=>{
getLastActive()
})
</script>

<TableModalCommonLayout
{isOpen}
title="Choose Your Active Members"
subheading1="Your new plan includes {maxSelectable} active members (plus you as the Hub Owner). Please select the team members to keep active."
subheading2="Unselected members will be removed from the Hub on {expiryDate}."
{maxSelectable}
stepIndicator={true}
selectedCount={selected.size}
isPrimaryDisabled={!isConfirmEnabled}
confirmText={confirmButtonText}
on:confirm={handleNext}
on:close={() => dispatch('close')}
>
<div slot="stepIndicator" class="flex items-center gap-2">
<span class="text-fs-ds-14 text-gray-400">Step 1</span>
<div class="mx-2 h-px w-12 bg-gray-600"></div>
<div class="flex items-center gap-1">
<div
class="text-fs-ds-12 flex h-5 w-5 items-center justify-center rounded-full bg-[#2B74FF] font-medium"
>
2
</div>
<span class="text-fs-ds-14 font-medium text-gray-300">Step 2</span>
</div>
</div>
<div class="flex-grow overflow-y-auto">
{#if filteredUsers.length > 0}
{#each filteredUsers as user}
<div
class="flex cursor-pointer items-center justify-between px-4 py-3 hover:bg-[#2A2F3A]"
on:click={() => toggleMember(user.id)}
>
<div class="flex items-center gap-3">
<input
type="checkbox"
checked={selected.has(user.id)}
on:click|stopPropagation={() => toggleMember(user.id)}
disabled={!selected.has(user.id) && selected.size >= maxSelectable}
class="peer h-4 w-4 rounded-sm border border-[#2A2F3A] bg-[#1E222C]"
/>
<div>
<span class="text-[12px] font-medium text-white">
{getFirstName(user.name || user.email)}
</span>
<span class="text-[12px] text-gray-400">({getRoleLabel(user.role)})</span>
<div class="truncate text-[11px] text-gray-500">{user.email}</div>
</div>
</div>
<div class="text-[12px] text-gray-400">
Last active: {getTimeDifference(user.lastActiveAt)}
</div>
</div>
{/each}
{:else}
<div class="flex items-center justify-center py-8 text-gray-400 text-sm italic">
No active members present for the selected workspaces.
</div>
{/if}
</div>

</TableModalCommonLayout>
198 changes: 198 additions & 0 deletions src/components/DowngradePlan/ChooseActiveWorkspaceModal.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
<script lang="ts">
import TableModalCommonLayout from './Common/TableModalCommonLayout.svelte';
import { createEventDispatcher, onMount } from 'svelte';
import { hubsService } from '@/services/hubs.service';

export let isOpen;
export let currentPlan;
export let selectedPlan;
export let workspaces = [];
export let hubId;
export let planLimits:[];

const dispatch = createEventDispatcher();
let selected = new Set();
let workspaceDetails = new Map();
let loading = false;

$: maxSelectable = planLimits?.workspacesPerHub?.value;

const toggleWorkspace = (id) => {
if (selected.has(id)) selected.delete(id);
else if (selected.size < maxSelectable) selected.add(id);
selected = new Set(selected);
};

const handleNext = async () => {
const selectedWorkspaces = workspaces.filter((ws) => selected.has(ws.id));
const unselected = workspaces.filter((ws) => !selected.has(ws.id));

const isDowngradeToCommunity = selectedPlan?.toLowerCase?.() === 'community';
let allMembers: any[] = [];

if (isDowngradeToCommunity && selectedWorkspaces.length) {
try {
const results = await Promise.all(
selectedWorkspaces.map(async (ws) => {
const res = await hubsService.getWorkspaceDetails({
workspaceId: ws.id,
hubId,
tab: 'members',
page: 1,
limit: 50,
sortBy: 'updatedAt',
sortOrder: 'desc',
});

const members = res?.data?.users || [];
return members.map((m) => ({
id: m._id,
email: m.email,
name: m.name || m.user || '',
role: m.role || 'member',
}));
}),
);

const flattened = results.flat();
const uniqueMembers = flattened.filter(
(user, i, self) =>
i ===
self.findIndex((u) => u.email === user.email || u.id === user.id)
);

allMembers = uniqueMembers.filter((u) => (u.role || '').toLowerCase() !== 'owner');

} catch (error) {
console.error('Error fetching workspace members:', error);
}
}

dispatch('next', {
selected: selectedWorkspaces,
unselected,
members: allMembers,
});
};


function getTimeDifference(updatedAt) {
const updatedDate = new Date(updatedAt);
const now = new Date();
const diffInDays = Math.floor((now - updatedDate) / (1000 * 60 * 60 * 24));
if (diffInDays === 0) return 'Today';
if (diffInDays === 1) return '1 day ago';
return `${diffInDays} days ago`;
}
onMount(async () => {
const promises = workspaces.map(async (ws) => {
try {
const res = await hubsService.getWorkspaceDetails({
workspaceId: ws.id,
});
} catch (error) {
console.error('Error fetching workspace details:', error);
}
});
});

onMount(async () => {
if (!workspaces?.length) return;
loading = true;
const promises = workspaces.map(async (ws) => {
try {
const res = await hubsService.getWorkspaceSummary({ workspaceId: ws.id, hubId });
const data = res?.data;
workspaceDetails.set(ws.id, {
collections: data?.totalCollections ?? '-',
contributors: data?.totalContributors ?? '-',
updated: data?.updatedAt ? getTimeDifference(data.updatedAt) : '-',
});
workspaceDetails = new Map(workspaceDetails);
} catch (err) {
console.error(`Failed to fetch ${ws.name}`, err);
}
});
await Promise.all(promises);
loading = false;
});
</script>

<TableModalCommonLayout
{isOpen}
title="Choose Your Active Workspaces"
subheading1="Your new plan includes {maxSelectable} active private workspaces. Please select the ones you want to keep active below."
subheading2="Unselected workspaces will be safely archived and can be restored anytime by upgrading."
{maxSelectable}
stepIndicator={maxSelectable === 3 ? true : false}
selectedCount={selected.size}
isPrimaryDisabled={selected.size !== maxSelectable}
confirmText="Confirm Selection"
on:confirm={handleNext}
on:close={() => dispatch('close')}
>
<div slot="stepIndicator" class="flex items-center gap-2">
<div class="flex items-center gap-1">
<div
class="text-fs-ds-12 flex h-5 w-5 items-center justify-center rounded-full bg-[#2B74FF] font-medium"
>
1
</div>
<span class="text-fs-ds-14 font-medium text-gray-300">Step 1</span>
</div>
<div class="mx-2 h-px w-12 border-t border-dotted bg-gray-600"></div>
<span class="text-fs-ds-14 text-gray-400">Step 2</span>
</div>
<!-- Slot: table content -->
<div class="flex-grow overflow-y-auto">
<table class="text-fs-ds-12 w-full border-collapse text-white">
<thead class="sticky top-0 z-10 bg-[#181C26] text-left text-gray-400">
<tr>
<th class="w-[20px] py-2"></th>
<th class="px-4 py-2">Workspaces</th>
<th class="px-4 py-2">Collections</th>
<th class="px-4 py-2">Contributors</th>
<th class="px-3 py-2">Last updated</th>
</tr>
</thead>
<tbody>
{#each workspaces as ws}
<tr
class="cursor-pointer transition-colors hover:bg-[#2A2F3A]"
on:click={() => toggleWorkspace(ws.id)}
>
<td class="relative px-2 py-2 text-center">
<label class="relative flex items-center justify-center">
<input
type="checkbox"
checked={selected.has(ws.id)}
on:change={() => toggleWorkspace(ws.id)}
disabled={!selected.has(ws.id) && selected.size >= maxSelectable}
class="peer h-4 w-4 cursor-pointer appearance-none rounded-sm border border-[#2A2F3A] bg-[#1E222C] checked:border-[#2B74FF] checked:bg-[#2B74FF] focus:outline-none"
/>
<!-- Checkmark icon -->
<svg
xmlns="http://www.w3.org/2000/svg"
class="pointer-events-none absolute h-3 w-3 text-white opacity-0 transition-opacity peer-checked:opacity-100"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M16.707 5.293a1 1 0 00-1.414 0L8 12.586 4.707 9.293a1 1 0 00-1.414 1.414l4 4a1 1 0 001.414 0l8-8a1 1 0 000-1.414z"
clip-rule="evenodd"
/>
</svg>
</label>
</td>

<td class="px-4 py-3 font-bold">{ws.name}</td>
<td class="px-4 py-3 font-bold">{workspaceDetails.get(ws.id)?.collections ?? '-'}</td>
<td class="px-4 py-3 font-bold">{workspaceDetails.get(ws.id)?.contributors ?? '-'}</td>
<td class="px-4 py-3 font-bold">{workspaceDetails.get(ws.id)?.updated ?? '-'}</td>
</tr>
{/each}
</tbody>
</table>
</div>
</TableModalCommonLayout>
Loading