* feat: Add read-only access support for Prompts - Backend: Add write_access field to PromptAccessResponse - Backend: Update /prompts/list to return prompts with write_access - Frontend: Display Read Only badge in Prompts list - Frontend: Disable inputs and save button when no write access * feat: Add read-only visual indicators for Prompts workspace * fix: Return write_access from getPromptByCommand endpoint - Backend returns write_access directly in response - Frontend extracts write_access from getPromptByCommand response - Remove inefficient getPromptList call in edit page * fix: Align Read Only badge to right in Prompts.svelte - Title and command stay on left - Badge pushed to right by justify-between * fix: Use PromptAccessResponse in get_prompt_by_command endpoint fix: Use PromptAccessResponse in get_prompt_by_command endpoint - Return PromptAccessResponse Pydantic model instead of raw dict - Properly type the response with response_model
This commit is contained in:
@@ -64,6 +64,10 @@ class PromptUserResponse(PromptModel):
|
||||
user: Optional[UserResponse] = None
|
||||
|
||||
|
||||
class PromptAccessResponse(PromptUserResponse):
|
||||
write_access: Optional[bool] = False
|
||||
|
||||
|
||||
class PromptForm(BaseModel):
|
||||
command: str
|
||||
title: str
|
||||
|
||||
@@ -4,6 +4,7 @@ from fastapi import APIRouter, Depends, HTTPException, status, Request
|
||||
from open_webui.models.prompts import (
|
||||
PromptForm,
|
||||
PromptUserResponse,
|
||||
PromptAccessResponse,
|
||||
PromptModel,
|
||||
Prompts,
|
||||
)
|
||||
@@ -31,14 +32,24 @@ async def get_prompts(user=Depends(get_verified_user), db: Session = Depends(get
|
||||
return prompts
|
||||
|
||||
|
||||
@router.get("/list", response_model=list[PromptUserResponse])
|
||||
@router.get("/list", response_model=list[PromptAccessResponse])
|
||||
async def get_prompt_list(user=Depends(get_verified_user), db: Session = Depends(get_session)):
|
||||
if user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL:
|
||||
prompts = Prompts.get_prompts(db=db)
|
||||
else:
|
||||
prompts = Prompts.get_prompts_by_user_id(user.id, "write", db=db)
|
||||
prompts = Prompts.get_prompts_by_user_id(user.id, "read", db=db)
|
||||
|
||||
return prompts
|
||||
return [
|
||||
PromptAccessResponse(
|
||||
**prompt.model_dump(),
|
||||
write_access=(
|
||||
(user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL)
|
||||
or user.id == prompt.user_id
|
||||
or has_access(user.id, "write", prompt.access_control, db=db)
|
||||
),
|
||||
)
|
||||
for prompt in prompts
|
||||
]
|
||||
|
||||
|
||||
############################
|
||||
@@ -87,7 +98,7 @@ async def create_new_prompt(
|
||||
############################
|
||||
|
||||
|
||||
@router.get("/command/{command}", response_model=Optional[PromptModel])
|
||||
@router.get("/command/{command}", response_model=Optional[PromptAccessResponse])
|
||||
async def get_prompt_by_command(command: str, user=Depends(get_verified_user), db: Session = Depends(get_session)):
|
||||
prompt = Prompts.get_prompt_by_command(f"/{command}", db=db)
|
||||
|
||||
@@ -97,15 +108,17 @@ async def get_prompt_by_command(command: str, user=Depends(get_verified_user), d
|
||||
or prompt.user_id == user.id
|
||||
or has_access(user.id, "read", prompt.access_control, db=db)
|
||||
):
|
||||
return prompt
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||
return PromptAccessResponse(
|
||||
**prompt.model_dump(),
|
||||
write_access=(
|
||||
(user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL)
|
||||
or user.id == prompt.user_id
|
||||
or has_access(user.id, "write", prompt.access_control, db=db)
|
||||
),
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
import XMark from '../icons/XMark.svelte';
|
||||
import GarbageBin from '../icons/GarbageBin.svelte';
|
||||
import ViewSelector from './common/ViewSelector.svelte';
|
||||
import Badge from '$lib/components/common/Badge.svelte';
|
||||
|
||||
let shiftKey = false;
|
||||
|
||||
@@ -327,11 +328,16 @@
|
||||
href={`/workspace/prompts/edit?command=${encodeURIComponent(prompt.command)}`}
|
||||
>
|
||||
<div class=" flex flex-col flex-1 space-x-4 cursor-pointer w-full pl-1">
|
||||
<div class=" flex-1 flex items-center gap-2 self-start">
|
||||
<div class=" font-medium line-clamp-1 capitalize">{prompt.title}</div>
|
||||
<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1 text-gray-500">
|
||||
{prompt.command}
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="font-medium line-clamp-1 capitalize">{prompt.title}</div>
|
||||
<div class="text-xs overflow-hidden text-ellipsis line-clamp-1 text-gray-500">
|
||||
{prompt.command}
|
||||
</div>
|
||||
</div>
|
||||
{#if !prompt.write_access}
|
||||
<Badge type="muted" content={$i18n.t('Read Only')} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class=" text-xs">
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
export let edit = false;
|
||||
export let prompt = null;
|
||||
export let clone = false;
|
||||
export let write_access = true;
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
@@ -40,6 +41,10 @@
|
||||
}
|
||||
|
||||
const submitHandler = async () => {
|
||||
if (!write_access) {
|
||||
toast.error($i18n.t('You do not have permission to edit this prompt.'));
|
||||
return;
|
||||
}
|
||||
loading = true;
|
||||
|
||||
if (validateCommandString(command)) {
|
||||
@@ -111,23 +116,30 @@
|
||||
placeholder={$i18n.t('Title')}
|
||||
bind:value={title}
|
||||
required
|
||||
disabled={!write_access}
|
||||
/>
|
||||
|
||||
<div class="self-center shrink-0">
|
||||
<button
|
||||
class="bg-gray-50 hover:bg-gray-100 text-black dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-white transition px-2 py-1 rounded-full flex gap-1 items-center"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
showAccessControlModal = true;
|
||||
}}
|
||||
>
|
||||
<LockClosed strokeWidth="2.5" className="size-3.5" />
|
||||
{#if write_access}
|
||||
<div class="self-center shrink-0">
|
||||
<button
|
||||
class="bg-gray-50 hover:bg-gray-100 text-black dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-white transition px-2 py-1 rounded-full flex gap-1 items-center"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
showAccessControlModal = true;
|
||||
}}
|
||||
>
|
||||
<LockClosed strokeWidth="2.5" className="size-3.5" />
|
||||
|
||||
<div class="text-sm font-medium shrink-0">
|
||||
{$i18n.t('Access')}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="text-sm font-medium shrink-0">
|
||||
{$i18n.t('Access')}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-xs shrink-0 text-gray-500">
|
||||
{$i18n.t('Read Only')}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex gap-0.5 items-center text-xs text-gray-500">
|
||||
@@ -138,7 +150,7 @@
|
||||
bind:value={command}
|
||||
on:input={handleCommandInput}
|
||||
required
|
||||
disabled={edit}
|
||||
disabled={edit || !write_access}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -158,6 +170,7 @@
|
||||
bind:value={content}
|
||||
rows={6}
|
||||
required
|
||||
disabled={!write_access}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -181,21 +194,22 @@
|
||||
</div>
|
||||
|
||||
<div class="my-4 flex justify-end pb-20">
|
||||
<button
|
||||
class=" text-sm w-full lg:w-fit px-4 py-2 transition rounded-xl {loading
|
||||
? ' cursor-not-allowed bg-black hover:bg-gray-900 text-white dark:bg-white dark:hover:bg-gray-100 dark:text-black'
|
||||
: 'bg-black hover:bg-gray-900 text-white dark:bg-white dark:hover:bg-gray-100 dark:text-black'} flex w-full justify-center"
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
>
|
||||
<div class=" self-center font-medium">{$i18n.t('Save & Create')}</div>
|
||||
|
||||
{#if loading}
|
||||
<div class="ml-1.5 self-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
<Tooltip content={!write_access ? $i18n.t('You do not have permission to save this prompt.') : ''}>
|
||||
<button
|
||||
class=" text-sm w-full lg:w-fit px-4 py-2 transition rounded-xl {loading || !write_access
|
||||
? ' cursor-not-allowed bg-gray-200 text-gray-500 dark:bg-gray-700 dark:text-gray-400'
|
||||
: 'bg-black hover:bg-gray-900 text-white dark:bg-white dark:hover:bg-gray-100 dark:text-black'} flex w-full justify-center"
|
||||
type="submit"
|
||||
disabled={loading || !write_access}
|
||||
>
|
||||
<div class=" self-center font-medium">{$i18n.t('Save & Create')}</div>
|
||||
{#if loading}
|
||||
<div class="ml-1.5 self-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
import PromptEditor from '$lib/components/workspace/Prompts/PromptEditor.svelte';
|
||||
|
||||
let prompt = null;
|
||||
let write_access = true;
|
||||
|
||||
const onSubmit = async (_prompt) => {
|
||||
console.log(_prompt);
|
||||
const prompt = await updatePromptByCommand(localStorage.token, _prompt).catch((error) => {
|
||||
@@ -38,6 +40,7 @@
|
||||
});
|
||||
|
||||
if (_prompt) {
|
||||
write_access = _prompt.write_access ?? true;
|
||||
prompt = {
|
||||
title: _prompt.title,
|
||||
command: _prompt.command,
|
||||
@@ -54,5 +57,5 @@
|
||||
</script>
|
||||
|
||||
{#if prompt}
|
||||
<PromptEditor {prompt} {onSubmit} edit />
|
||||
<PromptEditor {prompt} {onSubmit} {write_access} edit />
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user