mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-24 08:01:38 +00:00
feat(frontend): allow workspace admin to set workspace color (#5032)
* feat(frontend): allow workspace admin to set workspace color * sqlx prep * sqlx prep * make border wider, add removed comment
This commit is contained in:
+6
@@ -122,6 +122,11 @@
|
||||
"ordinal": 23,
|
||||
"name": "mute_critical_alerts",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 24,
|
||||
"name": "color",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -153,6 +158,7 @@
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT workspace.id, workspace.name, workspace.owner, workspace.deleted, workspace.premium, workspace_settings.color\n FROM workspace\n LEFT JOIN workspace_settings ON workspace.id = workspace_settings.workspace_id\n JOIN usr ON usr.workspace_id = workspace.id\n WHERE usr.email = $1 AND workspace.deleted = false",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "owner",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "deleted",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "premium",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "color",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "3651ed42be75d41ab0387f1551012d72c90235bb40d2f66e6fb235c990d78352"
|
||||
}
|
||||
+6
@@ -122,6 +122,11 @@
|
||||
"ordinal": 23,
|
||||
"name": "mute_critical_alerts",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 24,
|
||||
"name": "color",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -153,6 +158,7 @@
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT workspace.id, workspace.name, usr.username, workspace_settings.color\n FROM workspace\n JOIN usr ON usr.workspace_id = workspace.id\n JOIN workspace_settings ON workspace_settings.workspace_id = workspace.id\n WHERE usr.email = $1 AND workspace.deleted = false",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "username",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "color",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "6c845f168b6265e6cf92e4d33c5409edfdc1847d0348df2ea55504ddaaa67736"
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE workspace_settings SET color = $1 WHERE workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "d2668c2d4ece82a3617f098fe993d3e218fa224bd0bb311e7db776c7fd69cf0b"
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO workspace_settings\n (workspace_id, color)\n VALUES ($1, $2)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "e54eb583f011cea6e4d533f4d2014cbf509b5afcc2647048c9bfc839d8f290ad"
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT workspace.id, workspace.name, workspace.owner, workspace.deleted, workspace.premium, workspace_settings.color\n FROM workspace\n LEFT JOIN workspace_settings ON workspace.id = workspace_settings.workspace_id\n LIMIT $1 OFFSET $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "owner",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "deleted",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "premium",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "color",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "eed16e356f3f36183c3db13fcc1950295e0d0fbdabb38434534fb3430eeddc25"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE workspace_settings DROP COLUMN color;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE workspace_settings ADD COLUMN color VARCHAR(7) DEFAULT NULL;
|
||||
@@ -1576,6 +1576,30 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/workspaces/change_workspace_color:
|
||||
post:
|
||||
summary: change workspace id
|
||||
operationId: changeWorkspaceColor
|
||||
tags:
|
||||
- workspace
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
color:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: status
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/users/whois/{username}:
|
||||
get:
|
||||
summary: whois
|
||||
@@ -1722,6 +1746,8 @@ paths:
|
||||
$ref: "#/components/schemas/WorkspaceDefaultScripts"
|
||||
mute_critical_alerts:
|
||||
type: boolean
|
||||
color:
|
||||
type: string
|
||||
required:
|
||||
- code_completion_enabled
|
||||
- automatic_billing
|
||||
@@ -12765,10 +12791,13 @@ components:
|
||||
type: string
|
||||
username:
|
||||
type: string
|
||||
color:
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
- username
|
||||
- color
|
||||
required:
|
||||
- email
|
||||
- workspaces
|
||||
@@ -12782,6 +12811,8 @@ components:
|
||||
type: string
|
||||
username:
|
||||
type: string
|
||||
color:
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
@@ -12797,6 +12828,8 @@ components:
|
||||
type: string
|
||||
domain:
|
||||
type: string
|
||||
color:
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
|
||||
@@ -106,6 +106,7 @@ pub fn workspaced_service() -> Router {
|
||||
.route("/leave", post(leave_workspace))
|
||||
.route("/get_workspace_name", get(get_workspace_name))
|
||||
.route("/change_workspace_name", post(change_workspace_name))
|
||||
.route("/change_workspace_color", post(change_workspace_color))
|
||||
.route(
|
||||
"/change_workspace_id",
|
||||
post(crate::workspaces_extra::change_workspace_id),
|
||||
@@ -158,6 +159,7 @@ struct Workspace {
|
||||
owner: String,
|
||||
deleted: bool,
|
||||
premium: bool,
|
||||
color: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(FromRow, Serialize, Debug)]
|
||||
@@ -186,6 +188,7 @@ pub struct WorkspaceSettings {
|
||||
pub automatic_billing: bool,
|
||||
pub default_scripts: Option<serde_json::Value>,
|
||||
pub mute_critical_alerts: Option<bool>,
|
||||
pub color: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(FromRow, Serialize, Debug)]
|
||||
@@ -263,6 +266,7 @@ struct CreateWorkspace {
|
||||
id: String,
|
||||
name: String,
|
||||
username: Option<String>,
|
||||
color: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -282,6 +286,7 @@ struct UserWorkspace {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub username: String,
|
||||
pub color: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -376,8 +381,11 @@ async fn list_workspaces(
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
let workspaces = sqlx::query_as!(
|
||||
Workspace,
|
||||
"SELECT workspace.* FROM workspace, usr WHERE usr.workspace_id = workspace.id AND \
|
||||
usr.email = $1 AND deleted = false",
|
||||
"SELECT workspace.id, workspace.name, workspace.owner, workspace.deleted, workspace.premium, workspace_settings.color
|
||||
FROM workspace
|
||||
LEFT JOIN workspace_settings ON workspace.id = workspace_settings.workspace_id
|
||||
JOIN usr ON usr.workspace_id = workspace.id
|
||||
WHERE usr.email = $1 AND workspace.deleted = false",
|
||||
authed.email
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
@@ -1331,7 +1339,10 @@ async fn list_workspaces_as_super_admin(
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
let workspaces = sqlx::query_as!(
|
||||
Workspace,
|
||||
"SELECT * FROM workspace LIMIT $1 OFFSET $2",
|
||||
"SELECT workspace.id, workspace.name, workspace.owner, workspace.deleted, workspace.premium, workspace_settings.color
|
||||
FROM workspace
|
||||
LEFT JOIN workspace_settings ON workspace.id = workspace_settings.workspace_id
|
||||
LIMIT $1 OFFSET $2",
|
||||
per_page as i32,
|
||||
offset as i32
|
||||
)
|
||||
@@ -1348,9 +1359,11 @@ async fn user_workspaces(
|
||||
let mut tx = db.begin().await?;
|
||||
let workspaces = sqlx::query_as!(
|
||||
UserWorkspace,
|
||||
"SELECT workspace.id, workspace.name, usr.username
|
||||
FROM workspace, usr WHERE usr.workspace_id = workspace.id AND usr.email = $1 AND deleted = \
|
||||
false",
|
||||
"SELECT workspace.id, workspace.name, usr.username, workspace_settings.color
|
||||
FROM workspace
|
||||
JOIN usr ON usr.workspace_id = workspace.id
|
||||
JOIN workspace_settings ON workspace_settings.workspace_id = workspace.id
|
||||
WHERE usr.email = $1 AND workspace.deleted = false",
|
||||
email
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
@@ -1430,9 +1443,10 @@ async fn create_workspace(
|
||||
.await?;
|
||||
sqlx::query!(
|
||||
"INSERT INTO workspace_settings
|
||||
(workspace_id)
|
||||
VALUES ($1)",
|
||||
nw.id
|
||||
(workspace_id, color)
|
||||
VALUES ($1, $2)",
|
||||
nw.id,
|
||||
nw.color,
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
@@ -1943,6 +1957,11 @@ struct ChangeWorkspaceName {
|
||||
new_name: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ChangeWorkspaceColor {
|
||||
color: Option<String>,
|
||||
}
|
||||
|
||||
async fn change_workspace_name(
|
||||
authed: ApiAuthed,
|
||||
Path(w_id): Path<String>,
|
||||
@@ -1977,6 +1996,32 @@ async fn change_workspace_name(
|
||||
Ok(format!("updated workspace name to {}", &rw.new_name))
|
||||
}
|
||||
|
||||
async fn change_workspace_color(
|
||||
authed: ApiAuthed,
|
||||
Path(w_id): Path<String>,
|
||||
Extension(db): Extension<DB>,
|
||||
Json(rw): Json<ChangeWorkspaceColor>,
|
||||
) -> Result<String> {
|
||||
require_admin(authed.is_admin, &authed.username)?;
|
||||
|
||||
let mut tx = db.begin().await?;
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE workspace_settings SET color = $1 WHERE workspace_id = $2",
|
||||
rw.color,
|
||||
&w_id
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(format!(
|
||||
"updated workspace color to {}",
|
||||
rw.color.as_deref().unwrap_or("no color")
|
||||
))
|
||||
}
|
||||
|
||||
async fn get_usage(Extension(db): Extension<DB>, Path(w_id): Path<String>) -> Result<String> {
|
||||
let usage = sqlx::query_scalar!(
|
||||
"
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
<script lang="ts">
|
||||
import { workspaceStore, usersWorkspaceStore } from '$lib/stores'
|
||||
import Button from '../common/button/Button.svelte'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { WorkspaceService } from '$lib/gen'
|
||||
import Modal from '../common/modal/Modal.svelte'
|
||||
import { Pen } from 'lucide-svelte'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
|
||||
let colorEnabled = false
|
||||
export let open = false
|
||||
|
||||
$: workspaceColor = $usersWorkspaceStore?.workspaces.find(w => w.id === $workspaceStore)?.color
|
||||
$: colorEnabled = !!workspaceColor
|
||||
$: colorEnabled && !workspaceColor && generateRandomColor()
|
||||
|
||||
function generateRandomColor() {
|
||||
const randomColor =
|
||||
'#' +
|
||||
Math.floor(Math.random() * 16777215)
|
||||
.toString(16)
|
||||
.padStart(6, '0')
|
||||
workspaceColor = randomColor
|
||||
}
|
||||
|
||||
async function changeWorkspaceColor() {
|
||||
const colorToSave = colorEnabled && workspaceColor ? workspaceColor : undefined
|
||||
open = false
|
||||
await WorkspaceService.changeWorkspaceColor({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: {
|
||||
color: colorToSave
|
||||
}
|
||||
})
|
||||
|
||||
usersWorkspaceStore.set(await WorkspaceService.listUserWorkspaces())
|
||||
|
||||
sendUserToast(`Workspace color updated.`)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<p class="font-semibold text-sm">Workspace Color</p>
|
||||
<div class="flex flex-row gap-0.5 items-center">
|
||||
{#if workspaceColor}
|
||||
<div
|
||||
class="w-5 h-5 rounded-full border border-gray-300 dark:border-gray-600"
|
||||
style="background-color: {workspaceColor}"
|
||||
/>
|
||||
{:else}
|
||||
<span class="text-xs text-secondary">No color set</span>
|
||||
{/if}
|
||||
<Button
|
||||
on:click={() => {
|
||||
open = true
|
||||
}}
|
||||
size="xs"
|
||||
spacingSize="xs2"
|
||||
color="light"
|
||||
iconOnly
|
||||
startIcon={{
|
||||
icon: Pen
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modal bind:open title="Change Workspace Color">
|
||||
<div class="flex flex-col gap-4">
|
||||
<label class="block">
|
||||
<span class="text-secondary text-sm">Workspace color</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<Toggle bind:checked={colorEnabled} options={{ right: 'Enable' }} />
|
||||
<input class="w-10" type="color" bind:value={workspaceColor} disabled={!colorEnabled} />
|
||||
<input
|
||||
type="text"
|
||||
class="w-24 text-sm"
|
||||
bind:value={workspaceColor}
|
||||
disabled={!colorEnabled}
|
||||
/>
|
||||
<Button on:click={generateRandomColor} size="xs" disabled={!colorEnabled}>Random</Button>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button
|
||||
size="sm"
|
||||
on:click={() => {
|
||||
changeWorkspaceColor()
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
@@ -11,7 +11,7 @@
|
||||
export let stopPropagationOnClick: boolean = false
|
||||
export let shortcut: string = ''
|
||||
export let notificationsCount: number = 0
|
||||
|
||||
export let color: string | null = null
|
||||
let dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
@@ -26,10 +26,12 @@
|
||||
'group flex items-center px-2 py-2 font-light rounded-md h-8 gap-3 w-full',
|
||||
lightMode
|
||||
? 'text-primary hover:bg-surface-hover '
|
||||
: ' hover:bg-[#2A3648] text-primary-inverse dark:text-primary',
|
||||
: 'hover:bg-[#2A3648] text-primary-inverse dark:text-primary',
|
||||
color ? 'border-4' : '',
|
||||
'transition-all',
|
||||
$$props.class
|
||||
)}
|
||||
style={color ? `border-color: ${color}; padding: 0 calc(0.5rem - 4px);` : ''}
|
||||
title={label}
|
||||
>
|
||||
{#if icon}
|
||||
@@ -50,7 +52,7 @@
|
||||
<span
|
||||
class={twMerge(
|
||||
'whitespace-pre truncate',
|
||||
lightMode ? 'text-primary' : 'text-primary-inverse dark:text-primary',
|
||||
lightMode ? 'text-primary' : 'text-primary-inverse dark:text-primary',
|
||||
'transition-all',
|
||||
$$props.class
|
||||
)}
|
||||
|
||||
@@ -20,10 +20,11 @@
|
||||
import { MenuItem } from '@rgossiaux/svelte-headlessui'
|
||||
import { isCloudHosted } from '$lib/cloud'
|
||||
import { initAllAiWorkspace } from '../copilot/lib'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export let isCollapsed: boolean = false
|
||||
|
||||
// When used outside of the side bar, where links to workspace settings and such don't make as much sense.
|
||||
// When used outside of the side bar, where links to workspace settings and such don't make as much sense.
|
||||
export let strictWorkspaceSelect = false
|
||||
|
||||
async function toggleSwitchWorkspace(id: string) {
|
||||
@@ -55,7 +56,13 @@
|
||||
|
||||
<Menu>
|
||||
<div slot="trigger">
|
||||
<MenuButton class="!text-xs" icon={Building} label={$workspaceStore ?? ''} {isCollapsed} />
|
||||
<MenuButton
|
||||
class="!text-xs"
|
||||
icon={Building}
|
||||
label={$workspaceStore ?? ''}
|
||||
{isCollapsed}
|
||||
color={$userWorkspaces.find((w) => w.id === $workspaceStore)?.color}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="divide-y" role="none">
|
||||
@@ -63,17 +70,29 @@
|
||||
{#each $userWorkspaces as workspace}
|
||||
<MenuItem>
|
||||
<button
|
||||
class="text-xs min-w-0 w-full overflow-hidden flex flex-col py-1.5
|
||||
{$workspaceStore === workspace.id
|
||||
? 'cursor-default bg-surface-selected'
|
||||
: 'cursor-pointer hover:bg-surface-hover'}"
|
||||
class={twMerge(
|
||||
'text-xs min-w-0 w-full overflow-hidden flex flex-col py-1.5',
|
||||
$workspaceStore === workspace.id
|
||||
? 'cursor-default bg-surface-selected'
|
||||
: 'cursor-pointer hover:bg-surface-hover'
|
||||
)}
|
||||
on:click={async () => {
|
||||
await toggleSwitchWorkspace(workspace.id)
|
||||
}}
|
||||
>
|
||||
<div class="text-primary pl-4 truncate text-left text-[1.2em]">{workspace.name}</div>
|
||||
<div class="text-tertiary font-mono pl-4 text-2xs whitespace-nowrap truncate text-left">
|
||||
{workspace.id}
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<div class="text-primary pl-4 truncate text-left text-[1.2em]">{workspace.name}</div>
|
||||
<div class="text-tertiary font-mono pl-4 text-2xs whitespace-nowrap truncate text-left">
|
||||
{workspace.id}
|
||||
</div>
|
||||
</div>
|
||||
{#if workspace.color}
|
||||
<div
|
||||
class="w-5 h-5 mr-2 rounded border border-gray-300 dark:border-gray-600"
|
||||
style="background-color: {workspace.color}"
|
||||
></div>
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
</MenuItem>
|
||||
@@ -93,19 +112,19 @@
|
||||
</div>
|
||||
{/if}
|
||||
{#if !strictWorkspaceSelect}
|
||||
<div class="py-1" role="none">
|
||||
<a
|
||||
href="{base}/user/workspaces"
|
||||
on:click={() => {
|
||||
localStorage.removeItem('workspace')
|
||||
}}
|
||||
class="text-primary block px-4 py-2 text-xs hover:bg-surface-hover hover:text-primary"
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
>
|
||||
All workspaces
|
||||
</a>
|
||||
</div>
|
||||
<div class="py-1" role="none">
|
||||
<a
|
||||
href="{base}/user/workspaces"
|
||||
on:click={() => {
|
||||
localStorage.removeItem('workspace')
|
||||
}}
|
||||
class="text-primary block px-4 py-2 text-xs hover:bg-surface-hover hover:text-primary"
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
>
|
||||
All workspaces
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
{#if ($userStore?.is_admin || $superadmin) && !strictWorkspaceSelect}
|
||||
<div class="py-1" role="none">
|
||||
|
||||
@@ -55,6 +55,7 @@ export const userWorkspaces: Readable<
|
||||
id: string
|
||||
name: string
|
||||
username: string
|
||||
color: string | null
|
||||
}>
|
||||
> = derived([usersWorkspaceStore, superadmin], ([store, superadmin]) => {
|
||||
const originalWorkspaces = store?.workspaces ?? []
|
||||
@@ -64,7 +65,8 @@ export const userWorkspaces: Readable<
|
||||
{
|
||||
id: 'admins',
|
||||
name: 'Admins',
|
||||
username: 'superadmin'
|
||||
username: 'superadmin',
|
||||
color: null
|
||||
}
|
||||
]
|
||||
} else {
|
||||
|
||||
@@ -37,10 +37,19 @@
|
||||
let codeCompletionEnabled = true
|
||||
let checking = false
|
||||
|
||||
let workspaceColor: string | null = null
|
||||
let colorEnabled = false
|
||||
|
||||
function generateRandomColor() {
|
||||
const randomColor = '#' + Math.floor(Math.random() * 16777215).toString(16).padStart(6, '0');
|
||||
workspaceColor = randomColor;
|
||||
}
|
||||
|
||||
$: id = name.toLowerCase().replace(/\s/gi, '-')
|
||||
|
||||
$: validateName(id)
|
||||
$: errorUser = validateUsername(username)
|
||||
$: colorEnabled && !workspaceColor && generateRandomColor()
|
||||
|
||||
async function validateName(id: string): Promise<void> {
|
||||
checking = true
|
||||
@@ -60,6 +69,7 @@
|
||||
requestBody: {
|
||||
id,
|
||||
name,
|
||||
color: colorEnabled && workspaceColor ? workspaceColor : undefined,
|
||||
username: automateUsernameCreation ? undefined : username
|
||||
}
|
||||
})
|
||||
@@ -186,6 +196,16 @@
|
||||
{/if}
|
||||
<input type="text" bind:value={id} class:input-error={errorId != ''} />
|
||||
</label>
|
||||
<label class="block pb-4">
|
||||
<span class="text-secondary text-sm">Workspace color</span>
|
||||
<span class="ml-5 text-tertiary text-xs">Color to identify the current workspace</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<Toggle bind:checked={colorEnabled} options={{ right: 'Enable' }} />
|
||||
{#if colorEnabled}<input class="w-10" type="color" bind:value={workspaceColor} disabled={!colorEnabled} />{/if}
|
||||
<input type="text" class="w-24 text-sm" bind:value={workspaceColor} disabled={!colorEnabled} />
|
||||
<Button on:click={generateRandomColor} size="xs" disabled={!colorEnabled}>Random</Button>
|
||||
</div>
|
||||
</label>
|
||||
{#if !automateUsernameCreation}
|
||||
<label class="block pb-4">
|
||||
<span class="text-secondary text-sm">Your username in that workspace</span>
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
|
||||
let invites: WorkspaceInvite[] = []
|
||||
let list_all_as_super_admin: boolean = false
|
||||
let workspaces: { id: string; name: string; username: string }[] | undefined = undefined
|
||||
let workspaces: { id: string; name: string; username: string; color?: string | null }[] | undefined = undefined
|
||||
|
||||
let userSettings: UserSettings
|
||||
let superadminSettings: SuperadminSettings
|
||||
@@ -190,11 +190,18 @@
|
||||
<label class="block pb-2">
|
||||
<button
|
||||
class="block w-full mx-auto py-1 px-2 rounded-md border
|
||||
shadow-sm text-sm font-normal mt-1 hover:ring-1 hover:ring-indigo-300"
|
||||
shadow-sm text-sm font-normal mt-1 hover:ring-1 hover:ring-indigo-300"
|
||||
on:click={async () => {
|
||||
speakFriendAndEnterWorkspace(workspace.id)
|
||||
}}
|
||||
><span class="font-mono">{workspace.id}</span> - {workspace.name} as
|
||||
>
|
||||
{#if workspace.color}
|
||||
<span
|
||||
class="inline-block w-3 h-3 mr-2 rounded-full border border-gray-400"
|
||||
style="background-color: {workspace.color}"
|
||||
></span>
|
||||
{/if}
|
||||
<span class="font-mono">{workspace.id}</span> - {workspace.name} as
|
||||
<span class="font-mono">{workspace.username}</span>
|
||||
{#if workspace['deleted']}
|
||||
<span class="text-red-500"> (archived)</span>
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
import { fade } from 'svelte/transition'
|
||||
import ChangeWorkspaceName from '$lib/components/settings/ChangeWorkspaceName.svelte'
|
||||
import ChangeWorkspaceId from '$lib/components/settings/ChangeWorkspaceId.svelte'
|
||||
import ChangeWorkspaceColor from '$lib/components/settings/ChangeWorkspaceColor.svelte'
|
||||
import {
|
||||
convertBackendSettingsToFrontendSettings,
|
||||
convertFrontendToBackendSetting,
|
||||
@@ -818,6 +819,7 @@
|
||||
<div class="flex flex-col gap-10">
|
||||
<ChangeWorkspaceName />
|
||||
<ChangeWorkspaceId />
|
||||
<ChangeWorkspaceColor />
|
||||
</div>
|
||||
|
||||
<PageHeader title="Export workspace" primary={false} />
|
||||
|
||||
Reference in New Issue
Block a user