mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-06 00:02:13 +00:00
* fix: portal the confirmation modal so drawers cannot cover it Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K9UCPLT4t8PmrWunjfFsPW * fix: log a folder acl grant under the permission it granted Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K9UCPLT4t8PmrWunjfFsPW * fix: keep a table's actions column at its right edge Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K9UCPLT4t8PmrWunjfFsPW * feat: edit a folder in a drawer that saves once Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K9UCPLT4t8PmrWunjfFsPW * refactor: call the people on a folder or item members Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K9UCPLT4t8PmrWunjfFsPW * fix: edit a folder against the workspace the drawer targets Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K9UCPLT4t8PmrWunjfFsPW * refactor: drop the now-unused sticky actions column Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K9UCPLT4t8PmrWunjfFsPW * docs: correct the script editor drawer's modal placement note Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K9UCPLT4t8PmrWunjfFsPW * fix: pin the actions column without losing the row's hover tint Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K9UCPLT4t8PmrWunjfFsPW * feat: show the pinned column's seam only while the table overflows Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K9UCPLT4t8PmrWunjfFsPW * fix: draw the pinned column's seam as a shadow so it does not scroll away Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K9UCPLT4t8PmrWunjfFsPW * fix: fade the pinned column's tint in step with its row Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K9UCPLT4t8PmrWunjfFsPW * refactor: address review nits on the folder editor and pinned cell Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K9UCPLT4t8PmrWunjfFsPW * fix: keep the folder draft across a user-store refresh Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K9UCPLT4t8PmrWunjfFsPW * refactor: extract and test the folder draft's dirty check and permission diff Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K9UCPLT4t8PmrWunjfFsPW * fix: stop the folder editor showing state the server refused Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K9UCPLT4t8PmrWunjfFsPW * fix: keep a folder draft that no request ever reached the server Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K9UCPLT4t8PmrWunjfFsPW * fix: keep unapplied folder edits dirty when a save partially fails * fix: block folder form edits while a save is in flight * fix: commit a typed folder label before save snapshots the draft * fix: count a typed folder label as an unsaved change * fix: keep escape in the label input from closing what encloses it * fix: capitalize folder table headers and drop a dead portal target * refactor: make the confirmation modal portal opt-in per call site * docs: name the stacking context that actually traps the discard dialog * fix: report a half-landed member removal so the baseline reconciles * feat: edit a group in a drawer that saves once * fix: freeze the group name once the group exists * fix: revoke the caller's own group acl last so the rest of the save is authorized * docs: state the group call-ordering invariant once * fix: report a failing post-save reload instead of dropping the rejection * fix: hand the folder list reload back so a failure is reported * fix: treat a rejected group create as inconclusive and catch a throwing onSaved * revert: stop inferring a group was created from its name being taken * fix: say when a failed group create may have saved the group anyway * fix: key the may-have-been-created hint on the name conflict, not the status * fix: skip the may-have-been-created hint when the group is known to exist * feat: open a folder's group member from its row * fix: stop showing the caller as an admin when the read failed * fix: give up the caller's own folder admin last, and label a create as one * fix: drop a folder member's acl before its owner entry * fix: remove a folder owner before their acl, and correct the rls rationale * docs: say the refusal is on the caller's last admin handle * fix: defer only the folder rows the caller is an admin through * docs: describe callerOwners as what the caller passes in * docs: drop the call-site restatement of the diff's own invariant * docs: record manager as a legacy group role * fix: treat a sent request as possibly committed when reconciling * fix: reconcile on any failed edit, and compare members as a set * fix: keep write access when only the reconcile read fails --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
170 lines
5.6 KiB
Svelte
170 lines
5.6 KiB
Svelte
<script lang="ts">
|
|
import { Button, Drawer, DrawerContent } from './common'
|
|
import ConfirmationModal from './common/confirmationModal/ConfirmationModal.svelte'
|
|
import FolderEditor from './FolderEditor.svelte'
|
|
import { Save } from 'lucide-svelte'
|
|
import { sendUserToast } from '$lib/toast'
|
|
|
|
let {
|
|
offset = 0,
|
|
disableChatOffset = false,
|
|
onSaved = undefined,
|
|
workspace = undefined
|
|
}: {
|
|
offset?: number
|
|
disableChatOffset?: boolean
|
|
onSaved?: (name: string, created: boolean) => void | Promise<void>
|
|
/** Edit a folder of this workspace rather than the active one. */
|
|
workspace?: string
|
|
} = $props()
|
|
|
|
let drawer: Drawer | undefined = $state()
|
|
let mode: 'edit' | 'new' = $state('edit')
|
|
let name: string = $state('')
|
|
let canSave = $state(false)
|
|
let unsaved = $state(false)
|
|
// An `edit` drawer whose folder turns out not to exist saves by creating it. Calling that
|
|
// Save would promise an update the click does not perform.
|
|
let exists = $state(false)
|
|
let saving = $state(false)
|
|
let confirmDiscardOpen = $state(false)
|
|
let discarding = $state(false)
|
|
let editor: { save: () => Promise<{ name: string; created: boolean } | undefined> } | undefined =
|
|
$state()
|
|
// Bumped per open so the editor reloads its draft from the folder it is now
|
|
// pointed at. Keying on `name` instead would remount on every keystroke of the
|
|
// name field in `new` mode.
|
|
let instance = $state(0)
|
|
|
|
function open(nextMode: 'edit' | 'new', folderName: string): void {
|
|
mode = nextMode
|
|
name = folderName
|
|
discarding = false
|
|
confirmDiscardOpen = false
|
|
exists = nextMode === 'edit'
|
|
// The remounted editor reports these on its first effect, which is a tick away. Until
|
|
// then the header would carry the last folder's answers.
|
|
canSave = false
|
|
unsaved = false
|
|
instance++
|
|
drawer?.openDrawer()
|
|
}
|
|
|
|
export function initEdit(folderName: string): void {
|
|
open('edit', folderName)
|
|
}
|
|
|
|
export function initNew(initialName: string = ''): void {
|
|
open('new', initialName)
|
|
}
|
|
|
|
/** The editor keeps its draft in memory only, so closing throws it away. */
|
|
function requestClose() {
|
|
// A save is already writing. `unsaved` only clears once it reloads, so closing here
|
|
// would offer to discard changes the in-flight requests are busy persisting — and
|
|
// confirming would close on that lie. Saving is the shorter wait; ignore the close.
|
|
if (saving) return
|
|
if (discarding || !unsaved) {
|
|
drawer?.closeDrawer()
|
|
return
|
|
}
|
|
confirmDiscardOpen = true
|
|
}
|
|
|
|
async function save() {
|
|
saving = true
|
|
try {
|
|
// `created` comes from the editor, not from `mode`: a folder whose row turned out
|
|
// not to exist is created from an `initEdit` drawer.
|
|
const saved = await editor?.save()
|
|
if (saved) {
|
|
// Callers reload a list here. Called from inside the chain, not before it, so a
|
|
// synchronous throw is caught too — thrown out of `save()` it would skip the
|
|
// close below and strand the drawer open on a folder that did save.
|
|
void Promise.resolve()
|
|
.then(() => onSaved?.(saved.name, saved.created))
|
|
.catch((e) => sendUserToast(e?.body ?? String(e), true))
|
|
// The editor reloads its baseline after saving, but that lands a tick
|
|
// later; close on our own authority rather than racing it.
|
|
discarding = true
|
|
// Belt and braces with the `saving` guard on the close paths: nothing that
|
|
// asked to discard may outlive a save that then succeeded.
|
|
confirmDiscardOpen = false
|
|
drawer?.closeDrawer()
|
|
}
|
|
} finally {
|
|
saving = false
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<Drawer
|
|
bind:this={drawer}
|
|
{offset}
|
|
{disableChatOffset}
|
|
on:close={() => {
|
|
// Escape and click-away close the drawer before asking. Reopening in the same
|
|
// tick is how the flow's script editor drawer handles this too: the close
|
|
// transition has not started, so nothing flickers.
|
|
if (saving) {
|
|
drawer?.openDrawer()
|
|
return
|
|
}
|
|
if (!discarding && unsaved) {
|
|
drawer?.openDrawer()
|
|
confirmDiscardOpen = true
|
|
}
|
|
}}
|
|
>
|
|
<DrawerContent title={exists ? `Folder ${name}` : 'Create folder'} on:close={requestClose}>
|
|
<!-- `save()` snapshots the draft and then awaits several requests. An edit landing in
|
|
that window would not be in the snapshot, and the drawer closes on success — so
|
|
it would be lost without ever being offered as unsaved. `inert` keeps the form
|
|
from taking one. -->
|
|
<div inert={saving} class={saving ? 'opacity-60 transition-opacity' : 'transition-opacity'}>
|
|
{#key instance}
|
|
<FolderEditor
|
|
bind:this={editor}
|
|
bind:name
|
|
{mode}
|
|
{workspace}
|
|
onCanSaveChange={(v) => (canSave = v)}
|
|
onUnsavedChange={(v) => (unsaved = v)}
|
|
onExistsChange={(v) => (exists = v)}
|
|
/>
|
|
{/key}
|
|
</div>
|
|
{#snippet actions()}
|
|
<Button
|
|
variant="accent"
|
|
unifiedSize="md"
|
|
startIcon={{ icon: Save }}
|
|
disabled={!canSave}
|
|
loading={saving}
|
|
on:click={save}
|
|
>
|
|
{exists ? 'Save' : 'Create'}
|
|
</Button>
|
|
{/snippet}
|
|
</DrawerContent>
|
|
</Drawer>
|
|
|
|
<!-- `alwaysPortal`: this drawer is opened from inside other drawers (the folder picker of a
|
|
resource or variable form), and that outer drawer is a stacking context this dialog
|
|
cannot climb out of on z-index alone. Left in place it paints under the drawer whose
|
|
unsaved changes it is asking about, which leaves that drawer impossible to close. -->
|
|
<ConfirmationModal
|
|
alwaysPortal
|
|
open={confirmDiscardOpen}
|
|
title="Unsaved changes detected"
|
|
confirmationText="Discard changes"
|
|
onCanceled={() => (confirmDiscardOpen = false)}
|
|
onConfirmed={() => {
|
|
confirmDiscardOpen = false
|
|
discarding = true
|
|
drawer?.closeDrawer()
|
|
}}
|
|
>
|
|
<span> Are you sure you want to discard the changes you have made to this folder? </span>
|
|
</ConfirmationModal>
|