Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
3341d31
Try prod release again
istarkov Dec 5, 2025
38b6894
Try fix
istarkov Dec 5, 2025
0f2dce9
test
kof Dec 8, 2025
114ff6c
test
kof Dec 9, 2025
336cb58
test
kof Dec 12, 2025
b04dbe3
context menu
kof Dec 15, 2025
9b4080c
fix paste and settings permissions
kof Dec 16, 2025
abe743f
convert instance
kof Dec 17, 2025
23e2d42
tokens conflict management
kof Dec 18, 2025
a995a1a
publish enh, commands panel enh, instances search
kof Dec 18, 2025
643dcff
publish enh, commands panel enh, instances search
kof Dec 18, 2025
147d59c
fix svg upload
kof Dec 27, 2025
ffdba89
media conditions
kof Jan 4, 2026
edd81e3
test
kof Dec 8, 2025
ebb6672
test
kof Dec 9, 2025
0398848
test
kof Dec 12, 2025
b2ff56b
context menu
kof Dec 15, 2025
b84e535
fix paste and settings permissions
kof Dec 16, 2025
2f62533
convert instance
kof Dec 17, 2025
0dfea09
tokens conflict management
kof Dec 18, 2025
87bb871
publish enh, commands panel enh, instances search
kof Dec 18, 2025
880dba1
publish enh, commands panel enh, instances search
kof Dec 18, 2025
9a23e03
fix svg upload
kof Dec 27, 2025
edb1c8b
media conditions
kof Jan 4, 2026
2e7086e
0.248.0
kof Jan 12, 2026
589ea3c
0.253.0
kof Feb 5, 2026
0bb6a98
redirects
kof Feb 19, 2026
2cd3453
fix: use valid CSS color space names in toValue()
kof Mar 24, 2026
9a6e564
lostpixel trigger
kof Mar 31, 2026
72510c0
redepploy
kof Apr 1, 2026
8de5b8a
::migrate::
kof Apr 8, 2026
c628ea0
wip workspaces ::migrate::
kof Apr 13, 2026
4c5af00
test
kof Apr 15, 2026
1f48697
wip workspaces ::migrate::
kof Apr 19, 2026
071cd9a
fix: show over-capacity warning when members exceed paid seats
kof Apr 19, 2026
db1a530
::migrate:: workspaces
kof Apr 19, 2026
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
24 changes: 19 additions & 5 deletions .github/actions/vercel/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,11 @@ runs:
export GITHUB_SHA=${{ inputs.sha }}
export GITHUB_REF_NAME=${{ inputs.ref-name }}

pnpx vercel build
if [ "${{ inputs.environment }}" = "production" ]; then
pnpx vercel build --prod
else
pnpx vercel build
fi
shell: bash

- name: Patch
Expand All @@ -103,17 +107,27 @@ runs:
- name: Deploy
id: deploy
run: |
pnpx vercel deploy \
--prebuilt \
--token ${{ inputs.vercel-token }} \
2> >(tee info.txt >&2) | tee domain.txt
if [ "${{ inputs.environment }}" = "production" ]; then
pnpx vercel deploy \
--prebuilt \
--prod \
--skip-domain \
--token ${{ inputs.vercel-token }} \
2> >(tee info.txt >&2) | tee domain.txt
else
pnpx vercel deploy \
--prebuilt \
--token ${{ inputs.vercel-token }} \
2> >(tee info.txt >&2) | tee domain.txt
fi

echo "domain=$(cat ./domain.txt)" >> $GITHUB_OUTPUT
echo "inspect-url=$(cat info.txt | grep 'Inspect:' | awk '{print $2}')" >> $GITHUB_OUTPUT

shell: bash

- name: Set Alias
if: ${{ inputs.environment != 'production' }}
id: alias
run: |
ALIAS="${{ steps.branch.outputs.value }}"
Expand Down
19 changes: 19 additions & 0 deletions .github/workflows/vercel-deploy-staging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,18 @@ jobs:
sha: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
environment: ${{ matrix.environment }}

- uses: ./.github/actions/vercel
if: matrix.environment == 'staging'
id: vercel-prod
name: Deploy to Vercel
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
ref-name: ${{ github.ref_name }}
sha: ${{ github.sha }}
environment: "production"

- name: Debug Vercel Outputs
run: |
echo "domain=${{ steps.vercel.outputs.domain }}"
Expand All @@ -99,6 +111,13 @@ jobs:
description: "[${{ matrix.environment }}] Vercel logs"
url: "${{ steps.vercel.outputs.inspect-url }}"

- uses: ./.github/actions/add-status
if: matrix.environment == 'staging'
with:
title: "⏰ [${{ matrix.environment }}] Vercel Production Inspection"
description: "[${{ matrix.environment }}] Vercel production logs"
url: "${{ steps.vercel-prod.outputs.inspect-url }}"

- uses: ./.github/actions/add-status
with:
title: "⭐ [${{ matrix.environment }}] Apps Webstudio URL"
Expand Down
44 changes: 0 additions & 44 deletions apps/builder/app/dashboard/workspace/manage-members-dialog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,48 +178,4 @@ describe("computeAvailableSeats", () => {
computeAvailableSeats(makeData({ maxSeats: 1, members }), optimistic)
).toBe(-2);
});

test("confirmedMaxSeats overrides stale server maxSeats when higher", () => {
// Scenario: user confirmed paying for 2 extra seats but webhook hasn't fired.
// Server still returns maxSeats=3, but confirmedMaxSeats=5.
const members = [
{
userId: "u1",
email: "a@example.com",
relation: "editors" as const,
createdAt: "",
username: "",
},
];
const pendingInvites = [
{
notificationId: "n1",
recipientId: "r1",
email: "b@example.com",
relation: "editors" as const,
createdAt: "",
},
{
notificationId: "n2",
recipientId: "r2",
email: "c@example.com",
relation: "editors" as const,
createdAt: "",
},
];
// maxSeats=3 (stale), but 1 member + 2 pending = 3 occupied → would be 0 available.
// confirmedMaxSeats=5 → effectiveMax=5, available = 5-1-2 = 2.
expect(
computeAvailableSeats(
makeData({ maxSeats: 3, members, pendingInvites }),
[],
5
)
).toBe(2);
});

test("confirmedMaxSeats is ignored when server maxSeats is already higher", () => {
// Webhook fired: server returns maxSeats=5, confirmedMaxSeats=4 (stale override).
expect(computeAvailableSeats(makeData({ maxSeats: 5 }), [], 4)).toBe(5);
});
});
81 changes: 44 additions & 37 deletions apps/builder/app/dashboard/workspace/manage-members-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
ScrollAreaNative,
Select,
Tooltip,
PanelBanner,
css,
theme,
} from "@webstudio-is/design-system";
Expand Down Expand Up @@ -254,18 +255,11 @@ const computeAvailableSeats = (
{ success: true }
>["data"]
| undefined,
optimisticPending: OptimisticPendingInvite[],
// Optimistic upper bound for maxSeats set when extra seats are confirmed,
// to avoid flickering while the Stripe webhook has not yet updated TransactionLog.
confirmedMaxSeats?: number
optimisticPending: OptimisticPendingInvite[]
): number | undefined => {
if (membersData === undefined) {
return;
}
const effectiveMaxSeats =
confirmedMaxSeats !== undefined
? Math.max(membersData.maxSeats, confirmedMaxSeats)
: membersData.maxSeats;
const knownEmails = new Set([
membersData.owner.email,
...membersData.members.map((m) => m.email ?? ""),
Expand All @@ -275,7 +269,7 @@ const computeAvailableSeats = (
(o) => !knownEmails.has(o.email)
).length;
return (
effectiveMaxSeats -
membersData.maxSeats -
membersData.members.length -
membersData.pendingInvites.length -
extraCount
Expand Down Expand Up @@ -435,10 +429,6 @@ export const ManageMembersDialog = ({
relation: Role;
extraSeats: number;
}>();
// Optimistic maxSeats ceiling: set when the user confirms extra-seat charges so
// the footer and confirmation gate stay correct while the Stripe webhook is in
// flight (TransactionLog not yet updated).
const [confirmedMaxSeats, setConfirmedMaxSeats] = useState<number>();
const formRef = useRef<HTMLFormElement>(null);

const { load, data } = trpcClient.workspace.listMembers.useQuery();
Expand All @@ -453,22 +443,23 @@ export const ManageMembersDialog = ({
}
}, [isOpen, isOwner, load, workspace.id]);

// Clear the optimistic override once the server reflects the paid seats.
useEffect(() => {
if (
membersData !== undefined &&
confirmedMaxSeats !== undefined &&
membersData.maxSeats >= confirmedMaxSeats
) {
setConfirmedMaxSeats(undefined);
}
}, [membersData, confirmedMaxSeats]);
const availableSeats = computeAvailableSeats(membersData, optimisticPending);

const availableSeats = computeAvailableSeats(
membersData,
optimisticPending,
confirmedMaxSeats
);
const syncSeatsMutation = trpcClient.workspace.syncSeats.useMutation();

const overCapacity =
availableSeats !== undefined && availableSeats < 0 ? -availableSeats : 0;

const handleSyncSeats = () => {
syncSeatsMutation.send({ workspaceId: workspace.id }, (result) => {
if (result && "error" in result) {
setErrors([result.error]);
return;
}
handleRefresh();
revalidator.revalidate();
});
};

const performInvite = async (emails: string[], relation: Role) => {
setErrors(undefined);
Expand Down Expand Up @@ -536,14 +527,6 @@ export const ManageMembersDialog = ({
onConfirm={async () => {
const confirm = pendingConfirm;
setPendingConfirm(undefined);
// Optimistically raise confirmedMaxSeats so the footer and the
// confirmation gate reflect the payment before the webhook fires.
setConfirmedMaxSeats((prev) =>
Math.max(
prev ?? 0,
(membersData?.maxSeats ?? 0) + confirm.extraSeats
)
);
await performInvite(confirm.emails, confirm.relation);
}}
/>
Expand All @@ -554,7 +537,6 @@ export const ManageMembersDialog = ({
onOpenChange(open);
if (open === false) {
setErrors(undefined);
setConfirmedMaxSeats(undefined);
}
}}
>
Expand Down Expand Up @@ -599,6 +581,31 @@ export const ManageMembersDialog = ({
</Flex>
</Flex>
)}
{isOwner && overCapacity > 0 && (
<PanelBanner variant="warning">
<Flex direction="column" gap="2">
<Text>
{`Your workspace has ${overCapacity} more member${overCapacity === 1 ? "" : "s"} than your plan covers. Non-owner members won't be able to access the workspace until this is resolved.`}
</Text>
<Flex gap="2">
<Button
color="dark"
onClick={handleSyncSeats}
state={
syncSeatsMutation.state !== "idle"
? "pending"
: undefined
}
>
{`Buy ${overCapacity} extra seat${overCapacity === 1 ? "" : "s"}`}
</Button>
<Text color="subtle" css={{ alignSelf: "center" }}>
{`or remove ${overCapacity} member${overCapacity === 1 ? "" : "s"}`}
</Text>
</Flex>
</Flex>
</PanelBanner>
)}
<ScrollAreaNative
css={{
maxHeight: 300,
Expand Down
4 changes: 1 addition & 3 deletions apps/builder/app/routes/_ui.dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -268,9 +268,7 @@ const DashboardRoute = () => {
}, []);

useEffect(() => {
if (search !== "") {
setSetting("lastDashboardSearch", search);
}
setSetting("lastDashboardSearch", search);
}, [search]);

// `useLoaderData` wraps the return in `JsonifyObject` which turns
Expand Down
14 changes: 14 additions & 0 deletions apps/builder/app/services/workspace-router.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,20 @@ export const workspaceRouter = router({
}
}),

syncSeats: procedure
.input(z.object({ workspaceId: z.string() }))
.mutation(async ({ input, ctx }) => {
try {
if (ctx.planFeatures.maxWorkspaces <= 1) {
throw new Error("Upgrade your plan to manage workspace seats.");
}
await syncOwnerSeats(input.workspaceId, ctx);
return { success: true as const };
} catch (error) {
return createErrorResponse(error);
}
}),

listMembers: procedure
.input(z.object({ workspaceId: z.string() }))
.query(async ({ input, ctx }) => {
Expand Down
1 change: 0 additions & 1 deletion packages/css-engine/src/core/to-value.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -446,7 +446,6 @@ describe("Convert WS CSS Values to native CSS strings", () => {
expect(value).toBe(expected);
});
}
});

test("color in tuple", () => {
const value = toValue({
Expand Down
2 changes: 1 addition & 1 deletion packages/css-engine/src/core/to-value.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ export const toValue = (
case "oklch":
return `oklch(${c1} ${c2} ${c3} / ${alpha})`;
// Fall back to color() function for less common color spaces.
// colorjs uses internal short names that differ from CSS predefined color space identifiers.
// Webstudio uses colorjs internal names; map to CSS predefined color space names.
case "p3":
return `color(display-p3 ${c1} ${c2} ${c3} / ${alpha})`;
case "a98rgb":
Expand Down
Loading