Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ On **Yours**, **Sort by** orders the list by **Last updated**, **Last opened**,

Opening a library workflow adds an independent copy to the project; the library's version is untouched. If the project already has a copy, opening the template asks what to do: **Open copy** switches to the existing copy, **Add new copy** makes another one, and **Replace copy** replaces the copy's graph with the library version while keeping its name. A replace can be undone in one step.

Changes to a project's workflows never change the library by themselves. **Save to library…**, in the editor's header, saves the workflow as a new library template with the name you give it, including its form and its current settings as defaults. For a workflow opened from one of your own templates, **Update library template** replaces that template instead. If the template has changed since you opened it, you can save as a new template instead, or review the current one and replace it deliberately. Replacing a template also affects workflows that use it through the **Call Saved Workflow** node.
Changes to a project's workflows never change the library by themselves. **Save to library…**, in the editor's header, saves the workflow as a new library template with the name you give it, including its form and its current settings as defaults. Owners can update their own linked saved workflow templates, and multi-user administrators can update any user's saved non-bundled template; bundled templates remain read-only. If the template has changed since you opened it, you can save as a new template instead, or review the current one and replace it deliberately. Replacing a template also affects workflows that use it through the **Call Saved Workflow** node.

Add comma-separated tags, such as `portrait, SDXL, upscaling`, in the workflow's **Details**; they become filters in the library. Each workflow also keeps its own undo history while the project is open.

Expand Down
66 changes: 40 additions & 26 deletions docs/src/content/docs/configuration/Multi-User Mode/user-guide.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ description: How to use InvokeAI in multi-user mode as an end user.
sidebar:
order: 3
---
import { Steps } from '@astrojs/starlight/components'

import { Steps } from '@astrojs/starlight/components';

## Overview

Expand Down Expand Up @@ -48,15 +49,17 @@ If you're the first person to access a fresh InvokeAI installation in multi-user
Now:

<Steps>
1. Enter your email address (this will be your login name)
2. Create a display name (this will be the name other users see)
3. Choose a strong password. The following criteria are required with `strict_password_checking: true`.
- At least 8 characters long
- Contains uppercase letters
- Contains lowercase letters
- Contains numbers
4. Confirm your password
5. Click **Create Administrator Account**

1. Enter your email address (this will be your login name).

2. Create a display name (this will be the name other users see).

3. Choose a strong password. With `strict_password_checking: true`, it must have at least 8 characters, including uppercase letters, lowercase letters, and numbers.

4. Confirm your password.

5. Click **Create Administrator Account**.

</Steps>

With `strict_password_checking` disabled, you'll be warned if you choose a
Expand All @@ -81,10 +84,15 @@ This will take you to the User Management screen...
The User Management screen also allows you to:

<Steps>

1. Temporarily change a user's status to Inactive, preventing them from logging in to Invoke.

2. Edit a user (by clicking on the pencil icon) to change the user's display name or password.

3. Permanently delete a user.

4. Grant a user Administrator privileges.

</Steps>

---
Expand Down Expand Up @@ -119,7 +127,7 @@ As a regular user, you can:
- Customize your UI preferences (theme, hotkeys, etc.)
- View available models (read-only access to Model Manager)
- Open boards other users have given them access to, and add to the ones they can contribute to
- View and use workflows marked as shared by other users
- Browse public workflows in the **Shared** library tab, including your own public workflows

You cannot:

Expand All @@ -144,7 +152,7 @@ Administrators have all regular user capabilities, plus:
- Full model management (add, delete, configure models)
- Create and manage user accounts
- View and manage all users' generation queues
- View and manage all users' boards, images, and workflows (including system-owned legacy content)
- View all users' workflows in the Library; rename, delete, and visibility controls are shown only to each workflow's owner
- Access system configuration
- Grant or revoke admin privileges

Expand All @@ -156,12 +164,12 @@ Administrators have all regular user capabilities, plus:

In multi-user mode, each user can create an unlimited number of boards and organize their images and assets as they see fit, in the Library or inside a project. What another user may do with your board depends on their **role** on it:

| Role | Can |
| --- | --- |
| **Viewer** | Browse the board, open and download its images and videos, copy them to their own boards, and use them in their own generations. |
| Role | Can |
| --------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| **Viewer** | Browse the board, open and download its images and videos, copy them to their own boards, and use them in their own generations. |
| **Contributor** | Everything a viewer can, and add their own images and videos to the board. They can remove, star or delete only what they added. |
| **Editor** | Everything a contributor can, and remove or star anyone's items, rename the board, set its cover and archive it. |
| **Owner** | Everything, including changing who has access, moving the board between the Library and a project, and deleting it. |
| **Editor** | Everything a contributor can, and remove or star anyone's items, rename the board, set its cover and archive it. |
| **Owner** | Everything, including changing who has access, moving the board between the Library and a project, and deleting it. |

A board's **general access** sets the role every other user gets:

Expand Down Expand Up @@ -189,14 +197,16 @@ If an InvokeAI instance was in multiuser mode and then restarted in single user

### Workflows

Each user has their own private workflow library. Workflows you create are visible only to you by default.
In multi-user mode, new workflows saved to the Library are private by default. To change a saved workflow's visibility, open **Yours**, select it, and use **Visibility** in the detail panel to choose **Private** or **Public**. A public workflow is visible to signed-in users of this same InvokeAI instance; it is not published to the public internet.

You can also choose **Public workflow** when using **Save to Library** to create a public Library copy of a project workflow. This does not make the project-only workflow public. Changing a Library record's visibility does not publish unsaved project edits; use **Update Template** to update the Library copy's content. **Update Template** preserves that copy's visibility.

You can share a workflow with other users by marking it as **shared** (public). Shared workflows appear in all users' workflow libraries and can be opened by anyone, but only the owner (or an administrator) can modify or delete them.
Public workflows appear in **Shared**, including your own public workflows. Callable public workflows can also appear in the **Call Saved Workflow** picker.

To share a workflow, open it and use the sharing controls to toggle its public/shared status.
Other users can browse, open, duplicate, or download your public workflows. In the Library, only the workflow's owner is offered rename, delete, and visibility controls.

:::caution[Preexisting workflows after enabling multi-user mode]
When you enable multi-user mode for the first time on an existing InvokeAI installation, all workflows that were created before multi-user mode was activated will appear in the **shared workflows** section. These preexisting workflows are owned by the internal "system" account and are visible to all users. Administrators can edit or delete these shared legacy workflows. Regular users can view and use them but cannot modify them.
:::note[Preexisting Library workflows]
User workflows already in the Library before per-user workflow ownership was introduced are migrated as public records owned by the internal system account. They appear in **Shared**. The Library does not offer rename, delete, or visibility controls for these records to other accounts, but users can still open, duplicate, or download them.
:::

### The Generation Queue
Expand Down Expand Up @@ -276,9 +286,13 @@ These settings are stored per-user and won't affect other users.
**Possible Causes:**

<Steps>
1. **Filter Applied:** Check if a filter is hiding content
2. **Wrong User:** Ensure you're logged in with the correct account
3. **Archived Board:** Check the "Show Archived" option

1. **Filter Applied:** Check if a filter is hiding content.

2. **Wrong User:** Ensure you're logged in with the correct account.

3. **Archived Board:** Check the "Show Archived" option.

</Steps>

**Solution:**
Expand Down Expand Up @@ -329,7 +343,7 @@ Not unless you give them access. Boards start **restricted**: only you and admin

### Can I share my workflows with others?

Yes. You can mark any workflow as shared (public), which makes it visible to all users. Other users can view and use shared workflows, but only you or an administrator can modify or delete them.
Yes. In multi-user mode, make a saved Library workflow public from **Yours**, or choose **Public workflow** when saving a new Library copy. Public workflows are visible to signed-in users of the same InvokeAI instance and appear in **Shared**. Other users can open, duplicate, or download them; only the owner is offered rename, delete, and visibility controls in the Library. For a project copy linked to a public workflow, its owner or a ready administrator can use **Update Template**; other users can choose **Save as new** to create their own Library workflow.

### How long do sessions last?

Expand Down
9 changes: 9 additions & 0 deletions invokeai/frontend/webv2/public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -5215,6 +5215,11 @@
"openProjectChanged": "The project changed while \"{{name}}\" was loading; nothing was added.",
"openProjectWorkflowHint": "Makes it the active workflow",
"previewGraph": "Preview graph",
"private": "Private",
"public": "Public",
"publicVisibilityHelp": "Visible to users of this InvokeAI instance.",
"publicVisibilityPartial": "The template was saved, but its public visibility could not be confirmed ({{message}}). Retry to make it public.",
"publicWorkflow": "Public workflow",
"projectWorkflowCount_one": "{{count}} workflow in this project",
"projectWorkflowCount_other": "{{count}} workflows in this project",
"remove": "Remove",
Expand Down Expand Up @@ -5261,6 +5266,7 @@
"savedCreatedBody": "Saved \"{{name}}\" to the library.",
"savedUpdatedBody": "Updated \"{{name}}\" in the library.",
"searchPlaceholder": "Search names, tags, or descriptions",
"shared": "Shared",
"sourceBundled": "From a bundled template (read-only)",
"sourceNone": "Not linked to a library template",
"sourceRevision": "Linked to library template at revision {{revision}}",
Expand All @@ -5278,6 +5284,9 @@
"thumbnailUpdateFailed": "Failed to update thumbnail",
"thumbnailUpdated": "Thumbnail updated",
"thumbnailUpdating": "Updating thumbnail…",
"visibility": "Visibility",
"visibilityUpdateFailed": "Failed to update workflow visibility",
"visibilityUnknown": "Visibility unknown",
"title": "Workflows",
"unavailableBundledBody": "\"{{name}}\" came from a bundled template, which cannot be changed. Save it as a new template instead.",
"unavailableForbiddenBody": "The library template behind \"{{name}}\" belongs to someone else. Save it as a new template instead.",
Expand Down
79 changes: 79 additions & 0 deletions invokeai/frontend/webv2/scripts/mock-backend-fixtures.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1469,6 +1469,85 @@ test('runtime config PATCH keeps the other writable field and validates the queu
});
});

test('workflow creation defaults public for single-user callers and private for multiuser callers', async () => {
const backend = await startMockBackend(0, { profile: 'empty' });

try {
const createWorkflow = (name) =>
getJson(backend, '/api/v1/workflows', {
body: JSON.stringify({ workflow: { name, nodes: [], tags: 'local' } }),
headers: { 'content-type': 'application/json' },
method: 'POST',
});

const singleUser = await createWorkflow('Single-user template');
assert.equal(singleUser.is_public, true);

await getJson(backend, '/__faults', {
body: JSON.stringify({ intermediatesCaller: 'user' }),
headers: { 'content-type': 'application/json' },
method: 'POST',
});

const multiuser = await createWorkflow('Multiuser template');
assert.equal(multiuser.is_public, false);
} finally {
await backend.close();
}
});

test('workflow visibility PATCH mirrors the shared tag without changing revision and filters lists', async () => {
const backend = await startMockBackend(0, { profile: 'empty' });

try {
const createWorkflow = (name) =>
getJson(backend, '/api/v1/workflows', {
body: JSON.stringify({ workflow: { name, nodes: [], tags: 'local' } }),
headers: { 'content-type': 'application/json' },
method: 'POST',
});
const singleUser = await createWorkflow('Visible template');
const target = await createWorkflow('Toggle template');
const revision = target.revision;
const patchVisibility = (isPublic) =>
getJson(backend, `/api/v1/workflows/i/${encodeURIComponent(target.workflow_id)}/is_public`, {
body: JSON.stringify({ is_public: isPublic }),
headers: { 'content-type': 'application/json' },
method: 'PATCH',
});
const listNames = async (isPublic) => {
const params = new URLSearchParams({ categories: 'user', is_public: String(isPublic), per_page: '100' });

return (await getJson(backend, `/api/v1/workflows/?${params}`)).items.map((item) => item.name).sort();
};
const tagsOf = (value) =>
String(value ?? '')
.split(',')
.map((tag) => tag.trim())
.filter(Boolean)
.sort();

const hidden = await patchVisibility(false);
assert.equal(hidden.is_public, false);
assert.equal(hidden.revision, revision);
assert.deepEqual(tagsOf(hidden.tags), ['local']);
assert.deepEqual(tagsOf(hidden.workflow.tags), ['local']);
assert.deepEqual(await listNames(true), ['Visible template']);
assert.deepEqual(await listNames(false), ['Toggle template']);

const shared = await patchVisibility(true);
assert.equal(shared.is_public, true);
assert.equal(shared.revision, revision);
assert.deepEqual(tagsOf(shared.tags), ['local', 'shared']);
assert.deepEqual(tagsOf(shared.workflow.tags), ['local', 'shared']);
assert.deepEqual(await listNames(true), ['Toggle template', 'Visible template']);
assert.deepEqual(await listNames(false), []);
assert.equal(singleUser.is_public, true);
} finally {
await backend.close();
}
});

test('hosted-provider credentials and the starter catalog agree with the hosted fixture records', async () => {
await withRepresentativeBackend(async (backend) => {
const { models } = await getJson(backend, '/api/v2/models/');
Expand Down
49 changes: 47 additions & 2 deletions invokeai/frontend/webv2/scripts/mock-backend.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2898,6 +2898,18 @@ export const startMockBackend = async (port, { profile = 'empty' } = {}) => {
.split(',')
.map((tag) => tag.trim())
.filter(Boolean);
const workflowTagsForVisibility = (value, isPublic) => {
const tags = String(value ?? '')
.split(',')
.map((tag) => tag.trim())
.filter((tag) => tag && tag !== 'shared');

if (isPublic) {
tags.push('shared');
}

return tags.join(',');
};

if (path === '/api/v1/workflows/tags') {
const categories = url.searchParams.getAll('categories');
Expand Down Expand Up @@ -2927,6 +2939,37 @@ export const startMockBackend = async (port, { profile = 'empty' } = {}) => {
return json(200, counts);
}

const visibilityMatch = /^\/api\/v1\/workflows\/i\/([^/]+)\/is_public$/.exec(path);
if (visibilityMatch) {
if (method !== 'PATCH') {
return json(405, { detail: `No mock for ${method} ${path}` });
}

const workflowId = decodeURIComponent(visibilityMatch[1]);
const workflow = state.workflows.get(workflowId);

if (!workflow) {
return json(404, { detail: 'Workflow not found' });
}
if (workflow.category === 'default') {
return json(403, { detail: 'Bundled workflows cannot be modified' });
}

const body = await readJsonBody(request);
if (typeof body.is_public !== 'boolean') {
return json(422, { detail: 'is_public must be a boolean' });
}

workflow.is_public = body.is_public;
workflow.tags = workflowTagsForVisibility(workflow.tags, body.is_public);
workflow.workflow = {
...workflow.workflow,
tags: workflowTagsForVisibility(workflow.workflow?.tags, body.is_public),
};

return json(200, workflowRecord(workflow));
}

const workflowMatch = /^\/api\/v1\/workflows\/i\/([^/]+)(\/opened_at|\/thumbnail)?$/.exec(path);
if (workflowMatch) {
const workflowId = decodeURIComponent(workflowMatch[1]);
Expand Down Expand Up @@ -3052,7 +3095,7 @@ export const startMockBackend = async (port, { profile = 'empty' } = {}) => {
category: 'user',
created_at: timestamp(state),
description: content.description ?? '',
is_public: true,
is_public: !isMultiuserCaller(faults),
name: content.name ?? '',
opened_at: null,
revision: 1,
Expand All @@ -3074,6 +3117,7 @@ export const startMockBackend = async (port, { profile = 'empty' } = {}) => {
const categories = url.searchParams.getAll('categories');
const query = url.searchParams.get('query')?.trim().toLocaleLowerCase() ?? '';
const tags = url.searchParams.getAll('tags');
const isPublic = url.searchParams.get('is_public');
// The API pages from 0; the client asks for page 0 first.
const page = Math.max(0, Number(url.searchParams.get('page') ?? 0) || 0);
const perPage = Math.max(1, Number(url.searchParams.get('per_page') ?? 20) || 20);
Expand All @@ -3091,7 +3135,8 @@ export const startMockBackend = async (port, { profile = 'empty' } = {}) => {
(workflow) =>
(categories.length === 0 || categories.includes(workflow.category)) &&
(!query || `${workflow.name} ${workflow.description}`.toLocaleLowerCase().includes(query)) &&
(tags.length === 0 || tags.some((tag) => workflowTags(workflow).includes(tag)))
(tags.length === 0 || tags.some((tag) => workflowTags(workflow).includes(tag))) &&
(isPublic === null || String(workflow.is_public) === isPublic)
)
.sort(
(left, right) =>
Expand Down
6 changes: 5 additions & 1 deletion invokeai/frontend/webv2/scripts/run-project-file-journey.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -733,7 +733,11 @@ const runWorkflowCollection = async ({ browser, contexts, errors, imported }) =>
record.data.workflows.entries.map((entry) => entry.document.name),
['Empty Workflow', 'Journey Saved']
);
assert.deepEqual(record.data.workflows.entries[1].source, { libraryWorkflowId: saved.workflow_id, revision: 1 });
assert.deepEqual(record.data.workflows.entries[1].source, {
libraryWorkflowId: saved.workflow_id,
ownerUserId: 'fixture-user',
revision: 1,
});
assert.equal(record.workspace.data.workflows.activeWorkflowId, record.data.workflows.entries[1].document.id);

// Restart: both workflows and the active selection come back from the server.
Expand Down
Loading
Loading