Big things, maybe all's better now
This commit is contained in:
@@ -175,11 +175,11 @@
|
||||
if (!file) return;
|
||||
|
||||
if (!file.type.startsWith("image/")) {
|
||||
toasts.error("Please select an image file.");
|
||||
toasts.error(m.toast_error_select_image());
|
||||
return;
|
||||
}
|
||||
if (file.size > 2 * 1024 * 1024) {
|
||||
toasts.error("Image must be under 2MB.");
|
||||
toasts.error(m.toast_error_image_too_large());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -193,7 +193,7 @@
|
||||
.upload(path, file, { upsert: true });
|
||||
|
||||
if (uploadError) {
|
||||
toasts.error("Failed to upload avatar.");
|
||||
toasts.error(m.toast_error_upload_avatar());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -209,15 +209,15 @@
|
||||
.eq("id", org.id);
|
||||
|
||||
if (dbError) {
|
||||
toasts.error("Failed to save avatar URL.");
|
||||
toasts.error(m.toast_error_save_avatar_url());
|
||||
return;
|
||||
}
|
||||
|
||||
avatarUrl = publicUrl;
|
||||
await invalidateAll();
|
||||
toasts.success("Avatar updated.");
|
||||
toasts.success(m.toast_success_avatar_updated());
|
||||
} catch (err) {
|
||||
toasts.error("Avatar upload failed.");
|
||||
toasts.error(m.toast_error_avatar_upload());
|
||||
} finally {
|
||||
isUploading = false;
|
||||
input.value = "";
|
||||
@@ -232,11 +232,11 @@
|
||||
.eq("id", org.id);
|
||||
|
||||
if (error) {
|
||||
toasts.error("Failed to remove avatar.");
|
||||
toasts.error(m.toast_error_remove_avatar());
|
||||
} else {
|
||||
avatarUrl = null;
|
||||
await invalidateAll();
|
||||
toasts.success("Avatar removed.");
|
||||
toasts.success(m.toast_success_avatar_removed());
|
||||
}
|
||||
isSaving = false;
|
||||
}
|
||||
@@ -254,12 +254,12 @@
|
||||
.eq("id", org.id);
|
||||
|
||||
if (error) {
|
||||
toasts.error("Failed to save settings.");
|
||||
toasts.error(m.toast_error_save_settings());
|
||||
} else if (orgSlug !== org.slug) {
|
||||
window.location.href = `/${orgSlug}/settings`;
|
||||
} else {
|
||||
await invalidateAll();
|
||||
toasts.success("Settings saved.");
|
||||
toasts.success(m.toast_success_settings_saved());
|
||||
}
|
||||
isSaving = false;
|
||||
}
|
||||
@@ -278,10 +278,10 @@
|
||||
.eq("id", org.id);
|
||||
|
||||
if (error) {
|
||||
toasts.error("Failed to save preferences.");
|
||||
toasts.error(m.toast_error_save_preferences());
|
||||
} else {
|
||||
await invalidateAll();
|
||||
toasts.success("Preferences saved.");
|
||||
toasts.success(m.toast_success_preferences_saved());
|
||||
}
|
||||
isSavingPrefs = false;
|
||||
}
|
||||
@@ -299,10 +299,10 @@
|
||||
.eq("id", org.id);
|
||||
|
||||
if (error) {
|
||||
toasts.error("Failed to save event defaults.");
|
||||
toasts.error(m.toast_error_save_event_defaults());
|
||||
} else {
|
||||
await invalidateAll();
|
||||
toasts.success("Event defaults saved.");
|
||||
toasts.success(m.toast_success_event_defaults_saved());
|
||||
}
|
||||
isSavingDefaults = false;
|
||||
}
|
||||
@@ -320,10 +320,10 @@
|
||||
.eq("id", org.id);
|
||||
|
||||
if (error) {
|
||||
toasts.error("Failed to save feature settings.");
|
||||
toasts.error(m.toast_error_save_features());
|
||||
} else {
|
||||
await invalidateAll();
|
||||
toasts.success("Feature settings saved.");
|
||||
toasts.success(m.toast_success_features_saved());
|
||||
}
|
||||
isSavingFeatures = false;
|
||||
}
|
||||
@@ -348,10 +348,10 @@
|
||||
.eq("id", org.id);
|
||||
|
||||
if (error) {
|
||||
toasts.error("Failed to save social links.");
|
||||
toasts.error(m.toast_error_save_social());
|
||||
} else {
|
||||
await invalidateAll();
|
||||
toasts.success("Social links saved.");
|
||||
toasts.success(m.toast_success_social_saved());
|
||||
}
|
||||
isSavingSocial = false;
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
}
|
||||
|
||||
async function disconnectOrgCalendar() {
|
||||
if (!confirm("Disconnect Google Calendar?")) return;
|
||||
if (!confirm(m.settings_confirm_disconnect_cal())) return;
|
||||
const { error } = await supabase
|
||||
.from("org_google_calendars")
|
||||
.delete()
|
||||
@@ -113,51 +113,107 @@
|
||||
<!-- Google Calendar -->
|
||||
<div class="bg-dark/30 border border-light/5 rounded-xl p-5">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="w-10 h-10 bg-white rounded-xl flex items-center justify-center shrink-0">
|
||||
<div
|
||||
class="w-10 h-10 bg-white rounded-xl flex items-center justify-center shrink-0"
|
||||
>
|
||||
<svg class="w-6 h-6" viewBox="0 0 24 24">
|
||||
<path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
|
||||
<path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
|
||||
<path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
|
||||
<path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
|
||||
<path
|
||||
fill="#4285F4"
|
||||
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
||||
/>
|
||||
<path
|
||||
fill="#34A853"
|
||||
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
||||
/>
|
||||
<path
|
||||
fill="#FBBC05"
|
||||
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
|
||||
/>
|
||||
<path
|
||||
fill="#EA4335"
|
||||
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="font-heading text-body-sm text-white">Google Calendar</h3>
|
||||
<h3 class="font-heading text-body-sm text-white">
|
||||
Google Calendar
|
||||
</h3>
|
||||
<p class="text-[11px] text-light/40 mt-0.5">
|
||||
Sync events between your organization and Google Calendar.
|
||||
</p>
|
||||
|
||||
{#if orgCalendar}
|
||||
<div class="mt-3 p-3 bg-green-500/10 border border-green-500/20 rounded-lg">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
|
||||
<div
|
||||
class="mt-3 p-3 bg-green-500/10 border border-green-500/20 rounded-lg"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col sm:flex-row sm:items-center justify-between gap-3"
|
||||
>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-[11px] font-medium text-green-400">Connected</p>
|
||||
<p class="text-body-sm text-white">{orgCalendar.calendar_name || "Google Calendar"}</p>
|
||||
<p class="text-[10px] text-light/40 truncate" title={orgCalendar.calendar_id}>{orgCalendar.calendar_id}</p>
|
||||
<p class="text-[10px] text-light/30 mt-1">Events sync both ways.</p>
|
||||
<p
|
||||
class="text-[11px] font-medium text-green-400"
|
||||
>
|
||||
Connected
|
||||
</p>
|
||||
<p class="text-body-sm text-white">
|
||||
{orgCalendar.calendar_name ||
|
||||
"Google Calendar"}
|
||||
</p>
|
||||
<p
|
||||
class="text-[10px] text-light/40 truncate"
|
||||
title={orgCalendar.calendar_id}
|
||||
>
|
||||
{orgCalendar.calendar_id}
|
||||
</p>
|
||||
<p class="text-[10px] text-light/30 mt-1">
|
||||
Events sync both ways.
|
||||
</p>
|
||||
<a
|
||||
href="https://calendar.google.com/calendar/u/0/r?cid={encodeURIComponent(orgCalendar.calendar_id)}"
|
||||
href="https://calendar.google.com/calendar/u/0/r?cid={encodeURIComponent(
|
||||
orgCalendar.calendar_id,
|
||||
)}"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="inline-flex items-center gap-1 text-[11px] text-blue-400 hover:text-blue-300 mt-1.5"
|
||||
>
|
||||
<span class="material-symbols-rounded" style="font-size: 14px;">open_in_new</span>
|
||||
<span
|
||||
class="material-symbols-rounded"
|
||||
style="font-size: 14px;"
|
||||
>open_in_new</span
|
||||
>
|
||||
Open in Google Calendar
|
||||
</a>
|
||||
</div>
|
||||
<Button variant="danger" size="sm" onclick={disconnectOrgCalendar}>Disconnect</Button>
|
||||
<Button
|
||||
variant="danger"
|
||||
size="sm"
|
||||
onclick={disconnectOrgCalendar}
|
||||
>Disconnect</Button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{:else if !serviceAccountEmail}
|
||||
<div class="mt-3 p-3 bg-yellow-500/10 border border-yellow-500/20 rounded-lg">
|
||||
<p class="text-[11px] text-yellow-400 font-medium">Setup required</p>
|
||||
<div
|
||||
class="mt-3 p-3 bg-yellow-500/10 border border-yellow-500/20 rounded-lg"
|
||||
>
|
||||
<p class="text-[11px] text-yellow-400 font-medium">
|
||||
Setup required
|
||||
</p>
|
||||
<p class="text-[10px] text-light/40 mt-1">
|
||||
A server administrator needs to configure the <code class="bg-light/10 px-1 rounded">GOOGLE_SERVICE_ACCOUNT_KEY</code> environment variable.
|
||||
A server administrator needs to configure the <code
|
||||
class="bg-light/10 px-1 rounded"
|
||||
>GOOGLE_SERVICE_ACCOUNT_KEY</code
|
||||
> environment variable.
|
||||
</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="mt-3">
|
||||
<Button size="sm" onclick={() => (showConnectModal = true)}>Connect Google Calendar</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onclick={() => (showConnectModal = true)}
|
||||
>Connect Google Calendar</Button
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -167,12 +223,19 @@
|
||||
<!-- Discord (coming soon) -->
|
||||
<div class="bg-dark/30 border border-light/5 rounded-xl p-5 opacity-40">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="w-10 h-10 bg-[#5865F2] rounded-xl flex items-center justify-center shrink-0">
|
||||
<span class="material-symbols-rounded text-white" style="font-size: 22px;">forum</span>
|
||||
<div
|
||||
class="w-10 h-10 bg-[#5865F2] rounded-xl flex items-center justify-center shrink-0"
|
||||
>
|
||||
<span
|
||||
class="material-symbols-rounded text-white"
|
||||
style="font-size: 22px;">forum</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h3 class="font-heading text-body-sm text-white">Discord</h3>
|
||||
<p class="text-[11px] text-light/40 mt-0.5">Get notifications in your Discord server.</p>
|
||||
<p class="text-[11px] text-light/40 mt-0.5">
|
||||
Get notifications in your Discord server.
|
||||
</p>
|
||||
<p class="text-[10px] text-light/30 mt-1">Coming soon</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -181,12 +244,19 @@
|
||||
<!-- Slack (coming soon) -->
|
||||
<div class="bg-dark/30 border border-light/5 rounded-xl p-5 opacity-40">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="w-10 h-10 bg-[#4A154B] rounded-xl flex items-center justify-center shrink-0">
|
||||
<span class="material-symbols-rounded text-white" style="font-size: 22px;">tag</span>
|
||||
<div
|
||||
class="w-10 h-10 bg-[#4A154B] rounded-xl flex items-center justify-center shrink-0"
|
||||
>
|
||||
<span
|
||||
class="material-symbols-rounded text-white"
|
||||
style="font-size: 22px;">tag</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h3 class="font-heading text-body-sm text-white">Slack</h3>
|
||||
<p class="text-[11px] text-light/40 mt-0.5">Get notifications in your Slack workspace.</p>
|
||||
<p class="text-[11px] text-light/40 mt-0.5">
|
||||
Get notifications in your Slack workspace.
|
||||
</p>
|
||||
<p class="text-[10px] text-light/30 mt-1">Coming soon</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
interface Props {
|
||||
supabase: SupabaseClient<Database>;
|
||||
orgId: string;
|
||||
orgName: string;
|
||||
userId: string;
|
||||
members: Member[];
|
||||
roles: OrgRole[];
|
||||
@@ -56,6 +57,7 @@
|
||||
let {
|
||||
supabase,
|
||||
orgId,
|
||||
orgName,
|
||||
userId,
|
||||
members = $bindable(),
|
||||
roles,
|
||||
@@ -102,6 +104,25 @@
|
||||
invites = [...invites, invite as Invite];
|
||||
inviteEmail = "";
|
||||
showInviteModal = false;
|
||||
|
||||
// Send invite email (fire-and-forget)
|
||||
const inviteUrl = `${window.location.origin}/invite/${(invite as Invite).token}`;
|
||||
fetch("/api/send-invite-email", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
email,
|
||||
orgName,
|
||||
inviteUrl,
|
||||
role: inviteRole,
|
||||
}),
|
||||
}).then((res) => {
|
||||
if (res.ok) {
|
||||
toasts.success(m.invite_email_sent({ email }));
|
||||
} else if (res.status !== 501) {
|
||||
toasts.error(m.toast_error_send_invite_email());
|
||||
}
|
||||
});
|
||||
} else if (error) {
|
||||
toasts.error(m.toast_error_invite({ error: error.message }));
|
||||
}
|
||||
@@ -298,34 +319,34 @@
|
||||
<Modal
|
||||
isOpen={showInviteModal}
|
||||
onClose={() => (showInviteModal = false)}
|
||||
title="Invite Member"
|
||||
title={m.settings_invite_title()}
|
||||
>
|
||||
<div class="flex flex-col gap-4">
|
||||
<Input
|
||||
variant="compact"
|
||||
type="email"
|
||||
label="Email address"
|
||||
label={m.settings_invite_email()}
|
||||
bind:value={inviteEmail}
|
||||
placeholder="colleague@example.com"
|
||||
placeholder={m.settings_invite_email_placeholder()}
|
||||
/>
|
||||
<Select
|
||||
variant="compact"
|
||||
label="Role"
|
||||
label={m.settings_invite_role()}
|
||||
bind:value={inviteRole}
|
||||
placeholder=""
|
||||
options={[
|
||||
{ value: "viewer", label: "Viewer - Can view content" },
|
||||
{ value: "viewer", label: m.settings_invite_role_viewer() },
|
||||
{
|
||||
value: "commenter",
|
||||
label: "Commenter - Can view and comment",
|
||||
label: m.settings_invite_role_commenter(),
|
||||
},
|
||||
{
|
||||
value: "editor",
|
||||
label: "Editor - Can create and edit content",
|
||||
label: m.settings_invite_role_editor(),
|
||||
},
|
||||
{
|
||||
value: "admin",
|
||||
label: "Admin - Can manage members and settings",
|
||||
label: m.settings_invite_role_admin(),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
@@ -335,7 +356,8 @@
|
||||
<button
|
||||
type="button"
|
||||
class="px-4 py-2 text-body-sm text-light/60 hover:text-white transition-colors"
|
||||
onclick={() => (showInviteModal = false)}>Cancel</button
|
||||
onclick={() => (showInviteModal = false)}
|
||||
>{m.btn_cancel()}</button
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
@@ -343,7 +365,7 @@
|
||||
class="px-4 py-2 bg-primary text-background rounded-xl font-body text-body-sm hover:bg-primary-hover transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
onclick={sendInvite}
|
||||
>
|
||||
{isSendingInvite ? "..." : "Send Invite"}
|
||||
{isSendingInvite ? "..." : m.settings_invite_send()}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -353,7 +375,7 @@
|
||||
<Modal
|
||||
isOpen={showMemberModal}
|
||||
onClose={() => (showMemberModal = false)}
|
||||
title="Edit Member"
|
||||
title={m.settings_edit_member()}
|
||||
>
|
||||
{#if selectedMember}
|
||||
{@const rawP = selectedMember.profiles}
|
||||
@@ -369,23 +391,24 @@
|
||||
/>
|
||||
<div>
|
||||
<p class="text-body-sm text-white">
|
||||
{memberProfile?.full_name || "No name"}
|
||||
{memberProfile?.full_name ||
|
||||
m.settings_members_no_name()}
|
||||
</p>
|
||||
<p class="text-[11px] text-light/40">
|
||||
{memberProfile?.email || "No email"}
|
||||
{memberProfile?.email || m.settings_members_no_email()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Select
|
||||
variant="compact"
|
||||
label="Role"
|
||||
label={m.settings_invite_role()}
|
||||
bind:value={selectedMemberRole}
|
||||
placeholder=""
|
||||
options={[
|
||||
{ value: "viewer", label: "Viewer" },
|
||||
{ value: "commenter", label: "Commenter" },
|
||||
{ value: "editor", label: "Editor" },
|
||||
{ value: "admin", label: "Admin" },
|
||||
{ value: "viewer", label: m.role_viewer() },
|
||||
{ value: "commenter", label: m.role_commenter() },
|
||||
{ value: "editor", label: m.role_editor() },
|
||||
{ value: "admin", label: m.role_admin() },
|
||||
]}
|
||||
/>
|
||||
<button
|
||||
@@ -393,7 +416,7 @@
|
||||
class="text-[11px] text-error hover:underline self-start"
|
||||
onclick={removeMember}
|
||||
>
|
||||
Remove from organization
|
||||
{m.settings_members_remove()}
|
||||
</button>
|
||||
<div
|
||||
class="flex items-center justify-end gap-3 pt-2 border-t border-light/5"
|
||||
@@ -401,12 +424,13 @@
|
||||
<button
|
||||
type="button"
|
||||
class="px-4 py-2 text-body-sm text-light/60 hover:text-white transition-colors"
|
||||
onclick={() => (showMemberModal = false)}>Cancel</button
|
||||
onclick={() => (showMemberModal = false)}
|
||||
>{m.btn_cancel()}</button
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="px-4 py-2 bg-primary text-background rounded-xl font-body text-body-sm hover:bg-primary-hover transition-colors"
|
||||
onclick={updateMemberRole}>Save</button
|
||||
onclick={updateMemberRole}>{m.btn_save()}</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -81,14 +81,14 @@
|
||||
];
|
||||
|
||||
const roleColors = [
|
||||
{ value: "#ef4444", label: "Red" },
|
||||
{ value: "#f59e0b", label: "Amber" },
|
||||
{ value: "#10b981", label: "Emerald" },
|
||||
{ value: "#3b82f6", label: "Blue" },
|
||||
{ value: "#6366f1", label: "Indigo" },
|
||||
{ value: "#8b5cf6", label: "Violet" },
|
||||
{ value: "#ec4899", label: "Pink" },
|
||||
{ value: "#6b7280", label: "Gray" },
|
||||
{ value: "#ef4444", label: m.role_color_red() },
|
||||
{ value: "#f59e0b", label: m.role_color_amber() },
|
||||
{ value: "#10b981", label: m.role_color_emerald() },
|
||||
{ value: "#3b82f6", label: m.role_color_blue() },
|
||||
{ value: "#6366f1", label: m.role_color_indigo() },
|
||||
{ value: "#8b5cf6", label: m.role_color_violet() },
|
||||
{ value: "#ec4899", label: m.role_color_pink() },
|
||||
{ value: "#6b7280", label: m.role_color_gray() },
|
||||
];
|
||||
|
||||
function openRoleModal(role?: OrgRole) {
|
||||
@@ -203,19 +203,29 @@
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
{#each roles as role}
|
||||
<div class="bg-dark/30 border border-light/5 rounded-2xl px-4 py-3 hover:border-light/10 transition-colors">
|
||||
<div
|
||||
class="bg-dark/30 border border-light/5 rounded-2xl px-4 py-3 hover:border-light/10 transition-colors"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<div
|
||||
class="w-2.5 h-2.5 rounded-full shrink-0"
|
||||
style="background-color: {role.color}"
|
||||
></div>
|
||||
<span class="text-body-sm font-medium text-white">{role.name}</span>
|
||||
<span class="text-body-sm font-medium text-white"
|
||||
>{role.name}</span
|
||||
>
|
||||
{#if role.is_system}
|
||||
<span class="text-[10px] text-light/30 bg-light/5 px-1.5 py-0.5 rounded-md">System</span>
|
||||
<span
|
||||
class="text-[10px] text-light/30 bg-light/5 px-1.5 py-0.5 rounded-md"
|
||||
>System</span
|
||||
>
|
||||
{/if}
|
||||
{#if role.is_default}
|
||||
<span class="text-[10px] text-primary bg-primary/10 px-1.5 py-0.5 rounded-md">Default</span>
|
||||
<span
|
||||
class="text-[10px] text-primary bg-primary/10 px-1.5 py-0.5 rounded-md"
|
||||
>Default</span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5">
|
||||
@@ -226,7 +236,11 @@
|
||||
onclick={() => openRoleModal(role)}
|
||||
title="Edit"
|
||||
>
|
||||
<span class="material-symbols-rounded" style="font-size: 18px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 18;">edit</span>
|
||||
<span
|
||||
class="material-symbols-rounded"
|
||||
style="font-size: 18px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 18;"
|
||||
>edit</span
|
||||
>
|
||||
</button>
|
||||
{/if}
|
||||
{#if !role.is_system}
|
||||
@@ -236,20 +250,32 @@
|
||||
onclick={() => deleteRole(role)}
|
||||
title="Delete"
|
||||
>
|
||||
<span class="material-symbols-rounded" style="font-size: 18px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 18;">delete</span>
|
||||
<span
|
||||
class="material-symbols-rounded"
|
||||
style="font-size: 18px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 18;"
|
||||
>delete</span
|
||||
>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
{#if role.permissions.includes("*")}
|
||||
<span class="text-[10px] bg-light/5 text-light/50 px-1.5 py-0.5 rounded-md">All Permissions</span>
|
||||
<span
|
||||
class="text-[10px] bg-light/5 text-light/50 px-1.5 py-0.5 rounded-md"
|
||||
>All Permissions</span
|
||||
>
|
||||
{:else}
|
||||
{#each role.permissions.slice(0, 6) as perm}
|
||||
<span class="text-[10px] bg-light/5 text-light/40 px-1.5 py-0.5 rounded-md">{perm}</span>
|
||||
<span
|
||||
class="text-[10px] bg-light/5 text-light/40 px-1.5 py-0.5 rounded-md"
|
||||
>{perm}</span
|
||||
>
|
||||
{/each}
|
||||
{#if role.permissions.length > 6}
|
||||
<span class="text-[10px] text-light/30">+{role.permissions.length - 6} more</span>
|
||||
<span class="text-[10px] text-light/30"
|
||||
>+{role.permissions.length - 6} more</span
|
||||
>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
@@ -266,7 +292,9 @@
|
||||
>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label for="role-name" class="text-body-sm text-light/60 font-body">Name</label>
|
||||
<label for="role-name" class="text-body-sm text-light/60 font-body"
|
||||
>Name</label
|
||||
>
|
||||
<input
|
||||
id="role-name"
|
||||
type="text"
|
||||
@@ -282,7 +310,8 @@
|
||||
{#each roleColors as color}
|
||||
<button
|
||||
type="button"
|
||||
class="w-6 h-6 rounded-full border-2 transition-all {newRoleColor === color.value
|
||||
class="w-6 h-6 rounded-full border-2 transition-all {newRoleColor ===
|
||||
color.value
|
||||
? 'border-white scale-110'
|
||||
: 'border-transparent hover:border-light/30'}"
|
||||
style="background-color: {color.value}"
|
||||
@@ -293,7 +322,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<span class="text-body-sm text-light/60 font-body">Permissions</span>
|
||||
<span class="text-body-sm text-light/60 font-body">Permissions</span
|
||||
>
|
||||
<div class="flex flex-col gap-2 max-h-64 overflow-y-auto">
|
||||
{#each permissionGroups as group}
|
||||
<div class="p-3 bg-dark/50 rounded-xl">
|
||||
@@ -307,11 +337,15 @@
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={newRolePermissions.includes(perm)}
|
||||
checked={newRolePermissions.includes(
|
||||
perm,
|
||||
)}
|
||||
onchange={() => togglePermission(perm)}
|
||||
class="rounded accent-primary"
|
||||
/>
|
||||
<span class="capitalize">{perm.split(".")[1]}</span>
|
||||
<span class="capitalize"
|
||||
>{perm.split(".")[1]}</span
|
||||
>
|
||||
</label>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -319,15 +353,25 @@
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-end gap-3 pt-2 border-t border-light/5">
|
||||
<button type="button" class="px-4 py-2 text-body-sm text-light/60 hover:text-white transition-colors" onclick={() => (showRoleModal = false)}>{m.btn_cancel()}</button>
|
||||
<div
|
||||
class="flex items-center justify-end gap-3 pt-2 border-t border-light/5"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="px-4 py-2 text-body-sm text-light/60 hover:text-white transition-colors"
|
||||
onclick={() => (showRoleModal = false)}>{m.btn_cancel()}</button
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
disabled={!newRoleName.trim() || isSavingRole}
|
||||
class="px-4 py-2 bg-primary text-background rounded-xl font-body text-body-sm hover:bg-primary-hover transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
onclick={saveRole}
|
||||
>
|
||||
{isSavingRole ? "..." : editingRole ? m.btn_save() : m.btn_create()}
|
||||
{isSavingRole
|
||||
? "..."
|
||||
: editingRole
|
||||
? m.btn_save()
|
||||
: m.btn_create()}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user