Skip to content

Commit c265185

Browse files
authored
feat: Add "can publish" to content editors (#4530)
## Description ref #3994 <img width="409" alt="image" src="https://github.com/user-attachments/assets/fb00f25b-cf29-4374-8d7d-39fcec26eb9c"> ## Steps for reproduction 1. click button 2. expect xyz ## Code Review - [ ] hi @kof, I need you to do - conceptual review (architecture, feature-correctness) - detailed review (read every line) - test it on preview ## Before requesting a review - [ ] made a self-review - [ ] added inline comments where things may be not obvious (the "why", not "what") ## Before merging - [ ] tested locally and on preview environment (preview dev login: 0000) - [ ] updated [test cases](https://github.com/webstudio-is/webstudio/blob/main/apps/builder/docs/test-cases.md) document - [ ] added tests - [ ] if any new env variables are added, added them to `.env` file
1 parent b82fcd4 commit c265185

File tree

11 files changed

+216
-46
lines changed

11 files changed

+216
-46
lines changed

apps/builder/app/builder/features/topbar/publish.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ import stripIndent from "strip-indent";
3838
import { $isPublishDialogOpen } from "../../shared/nano-states";
3939
import { validateProjectDomain, type Project } from "@webstudio-is/project";
4040
import {
41-
$authPermit,
41+
$authTokenPermissions,
4242
$project,
4343
$publishedOrigin,
4444
$userPlanFeatures,
@@ -837,16 +837,16 @@ type PublishProps = {
837837

838838
export const PublishButton = ({ projectId }: PublishProps) => {
839839
const isPublishDialogOpen = useStore($isPublishDialogOpen);
840-
const authPermit = useStore($authPermit);
840+
const authTokenPermissions = useStore($authTokenPermissions);
841841
const [dialogContentType, setDialogContentType] = useState<
842842
"publish" | "export"
843843
>("publish");
844844

845-
const isPublishEnabled = authPermit === "own" || authPermit === "admin";
845+
const isPublishEnabled = authTokenPermissions.canPublish;
846846

847847
const tooltipContent = isPublishEnabled
848848
? undefined
849-
: "Only owner or admin can publish projects";
849+
: "Only the owner, an admin, or content editors with publish permissions can publish projects";
850850

851851
const handleExportClick = () => {
852852
setDialogContentType("export");

apps/builder/app/shared/nano-states/misc.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,7 @@ export const $authPermit = atom<AuthPermit>("view");
331331
export const $authTokenPermissions = atom<TokenPermissions>({
332332
canClone: true,
333333
canCopy: true,
334+
canPublish: false,
334335
});
335336

336337
export const $authToken = atom<string | undefined>(undefined);

apps/builder/app/shared/share-project/share-project.stories.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,20 +19,23 @@ const initialLinks: Array<LinkOptions> = [
1919
relation: "viewers",
2020
canClone: false,
2121
canCopy: false,
22+
canPublish: false,
2223
},
2324
{
2425
token: crypto.randomUUID(),
2526
name: "View and Edit",
2627
relation: "editors",
2728
canClone: false,
2829
canCopy: false,
30+
canPublish: false,
2931
},
3032
{
3133
token: crypto.randomUUID(),
3234
name: "Build",
3335
relation: "builders",
3436
canClone: false,
3537
canCopy: false,
38+
canPublish: false,
3639
},
3740
];
3841

@@ -63,6 +66,7 @@ const useShareProject = (
6366
relation: "viewers",
6467
canClone: false,
6568
canCopy: false,
69+
canPublish: false,
6670
},
6771
]);
6872
};

apps/builder/app/shared/share-project/share-project.tsx

Lines changed: 67 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ type MenuProps = {
9494
};
9595

9696
const Menu = ({ name, hasProPlan, value, onChange, onDelete }: MenuProps) => {
97-
const ids = useIds(["name", "canClone", "canCopy"]);
97+
const ids = useIds(["name", "canClone", "canCopy", "canPublish"]);
9898
const [isOpen, setIsOpen] = useState(false);
9999
const [customLinkName, setCustomLinkName] = useState<string>(name);
100100

@@ -234,36 +234,71 @@ const Menu = ({ name, hasProPlan, value, onChange, onDelete }: MenuProps) => {
234234
</Grid>
235235

236236
{isFeatureEnabled("contentEditableMode") && (
237-
<Permission
238-
disabled={hasProPlan !== true}
239-
onCheckedChange={handleCheckedChange("editors")}
240-
checked={value.relation === "editors"}
241-
title="Content"
242-
info={
243-
<Flex direction="column">
244-
Recipients can edit content only, such as text, images, and
245-
predefined components.
246-
{hasProPlan !== true && (
247-
<>
248-
<br />
249-
<br />
250-
Upgrade to a Pro account to share with Content Edit
251-
permissions.
252-
<br /> <br />
253-
<Link
254-
className={buttonStyle({ color: "gradient" })}
255-
color="contrast"
256-
underline="none"
257-
href="https://webstudio.is/pricing"
258-
target="_blank"
259-
>
260-
Upgrade
261-
</Link>
262-
</>
263-
)}
264-
</Flex>
265-
}
266-
/>
237+
<>
238+
<Permission
239+
disabled={hasProPlan !== true}
240+
onCheckedChange={handleCheckedChange("editors")}
241+
checked={value.relation === "editors"}
242+
title="Content"
243+
info={
244+
<Flex direction="column">
245+
Recipients can edit content only, such as text, images, and
246+
predefined components.
247+
{hasProPlan !== true && (
248+
<>
249+
<br />
250+
<br />
251+
Upgrade to a Pro account to share with Content Edit
252+
permissions.
253+
<br /> <br />
254+
<Link
255+
className={buttonStyle({ color: "gradient" })}
256+
color="contrast"
257+
underline="none"
258+
href="https://webstudio.is/pricing"
259+
target="_blank"
260+
>
261+
Upgrade
262+
</Link>
263+
</>
264+
)}
265+
</Flex>
266+
}
267+
/>
268+
<Grid
269+
css={{
270+
ml: theme.spacing[6],
271+
}}
272+
>
273+
<Grid
274+
gap={1}
275+
flow={"column"}
276+
css={{
277+
alignItems: "center",
278+
justifyContent: "start",
279+
}}
280+
>
281+
<Checkbox
282+
disabled={
283+
hasProPlan !== true || value.relation !== "editors"
284+
}
285+
checked={value.canPublish}
286+
onCheckedChange={(canPublish) => {
287+
onChange({ ...value, canPublish: Boolean(canPublish) });
288+
}}
289+
id={ids.canPublish}
290+
/>
291+
<Label
292+
htmlFor={ids.canPublish}
293+
disabled={
294+
hasProPlan !== true || value.relation !== "editors"
295+
}
296+
>
297+
Can publish
298+
</Label>
299+
</Grid>
300+
</Grid>
301+
</>
267302
)}
268303

269304
<Permission
@@ -336,6 +371,7 @@ export type LinkOptions = {
336371
relation: Relation;
337372
canCopy: boolean;
338373
canClone: boolean;
374+
canPublish: boolean;
339375
};
340376

341377
type SharedLinkItemType = {

packages/authorization-token/src/db/authorization-token.ts

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,42 @@ type AuthorizationToken =
1111
const applyTokenPermissions = (
1212
token: AuthorizationToken
1313
): AuthorizationToken => {
14+
let result = token;
15+
16+
// @todo: fix this on SQL level
1417
if (token.relation !== "viewers") {
15-
return {
16-
...token,
18+
result = {
19+
...result,
1720
canClone: true,
1821
canCopy: true,
1922
};
2023
}
2124

22-
return token;
25+
// @todo: fix this on SQL level
26+
if (token.relation === "viewers") {
27+
result = {
28+
...result,
29+
canPublish: false,
30+
};
31+
}
32+
33+
// @todo: fix this on SQL level
34+
if (token.relation === "builders") {
35+
result = {
36+
...result,
37+
canPublish: false,
38+
};
39+
}
40+
41+
// @todo: fix this on SQL level
42+
if (token.relation === "administrators") {
43+
result = {
44+
...result,
45+
canPublish: true,
46+
};
47+
}
48+
49+
return result;
2350
};
2451

2552
export const findMany = async (
@@ -55,6 +82,7 @@ export const findMany = async (
5582
export const tokenDefaultPermissions = {
5683
canClone: true,
5784
canCopy: true,
85+
canPublish: true,
5886
};
5987

6088
export type TokenPermissions = typeof tokenDefaultPermissions;
@@ -89,6 +117,7 @@ export const getTokenPermissions = async (
89117
return {
90118
canClone: dbToken.canClone,
91119
canCopy: dbToken.canCopy,
120+
canPublish: dbToken.canPublish,
92121
};
93122
};
94123

@@ -168,6 +197,7 @@ export const update = async (
168197
relation: props.relation,
169198
canClone: props.canClone,
170199
canCopy: props.canCopy,
200+
canPublish: props.canPublish,
171201
})
172202
.eq("projectId", projectId)
173203
.eq("token", props.token)

packages/authorization-token/src/trpc/authorization-tokens-router.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ export const authorizationTokenRouter = router({
6868
relation: TokenProjectRelation,
6969
canClone: z.boolean(),
7070
canCopy: z.boolean(),
71+
canPublish: z.boolean(),
7172
})
7273
)
7374
.mutation(async ({ input, ctx }) => {
@@ -77,6 +78,7 @@ export const authorizationTokenRouter = router({
7778
token: input.token,
7879
name: input.name,
7980
relation: input.relation,
81+
canPublish: input.canPublish,
8082
canClone: input.canClone,
8183
canCopy: input.canCopy,
8284
},

0 commit comments

Comments
 (0)