feat: Prompts Atomic PR of #20243 (#20368)

* 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:
Classic298
2026-01-05 01:36:13 +01:00
committed by GitHub
parent b55a46ae99
commit 1f059fe730
5 changed files with 85 additions and 45 deletions

View File

@@ -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

View File

@@ -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,
)

View File

@@ -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">

View File

@@ -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>

View File

@@ -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}