All files / src/components/projects project-actions.ts

100% Statements 5/5
100% Branches 22/22
100% Functions 1/1
100% Lines 5/5

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156                                                                                                                                          88x 88x 88x   88x                                                                                                 10x                                                                  
/**
 * The project action set, decided once.
 *
 * WHAT WAS WRONG
 * `ProjectHeader` offered Publish only on a `draft` and Archive only on a
 * `published` project — so **an archived project's detail page had no actions
 * at all**: a dead end. Unarchive and Delete existed, but only on the list
 * page, where the default status filter excluded `archived`, so the one row
 * that could have offered them never rendered. Unarchive is the inverse of an
 * action the product already has; a status you can enter and not leave is a
 * bug, not a design.
 *
 * WHAT THIS IS
 * `project-detail-details.html` draws the `[ ⋯ ]` menu OPEN to make the point:
 * **every action is listed at every status**, with a printed reason on the ones
 * that do not apply right now. "Every action is listed at every status — an
 * archived project can always come back."
 *
 * A disabled item with a reason beats a hidden one. A hidden item teaches the
 * pro that the action does not exist; a disabled one with "not archived" next
 * to it teaches them when it will.
 */
 
/** Stable ids. The menu keys copy and confirmation text off these. */
export type ProjectActionId =
	| "publish"
	| "move-to-draft"
	| "archive"
	| "unarchive"
	| "delete";
 
export interface ProjectAction {
	id: ProjectActionId;
	label: string;
	/** Why it cannot be used right now. Printed next to a disabled item. */
	reason?: string;
	enabled: boolean;
	/** Printed on the danger ink, below a separator. */
	danger?: boolean;
}
 
export interface ProjectActionInput {
	status: string;
	/**
	 * The server's blocking requirement label when the publish bar refuses this
	 * project — `apps/api/src/lib/publish-readiness.ts` owns the wording.
	 * Undefined when the bar is clear.
	 */
	publishBlockedBy?: string;
	/**
	 * Whether the back end behind this screen can move a project back to draft.
	 * The pro API has `POST …/unarchive`, which sets `draft` from either
	 * `published` or `archived`; the admin API has no equivalent, so the admin
	 * screen prints the two items with a reason rather than offering them.
	 */
	canMoveToDraft: boolean;
	/** Whether this screen may delete. */
	canDelete: boolean;
}
 
/**
 * The five items, in the mockup's order, every one of them present.
 */
export function projectActions({
	status,
	publishBlockedBy,
	canMoveToDraft,
	canDelete,
}: ProjectActionInput): ProjectAction[] {
	const isPublished = status === "published";
	const isDraft = status === "draft";
	const isArchived = status === "archived";
 
	return [
		{
			id: "publish",
			label: "Publish",
			enabled: !isPublished && !publishBlockedBy,
			reason: isPublished ? "already live" : publishBlockedBy,
		},
		{
			id: "move-to-draft",
			label: "Move to draft",
			enabled: isPublished && canMoveToDraft,
			reason: isDraft
				? "already a draft"
				: isArchived
					? "archived — unarchive instead"
					: canMoveToDraft
						? undefined
						: "not available here",
		},
		{
			id: "archive",
			label: "Archive",
			enabled: !isArchived,
			reason: isArchived ? "already archived" : undefined,
		},
		{
			id: "unarchive",
			label: "Unarchive",
			enabled: isArchived && canMoveToDraft,
			reason: !isArchived
				? "not archived"
				: canMoveToDraft
					? undefined
					: "not available here",
		},
		{
			id: "delete",
			label: "Delete permanently",
			enabled: canDelete,
			reason: canDelete ? undefined : "not available here",
			danger: true,
		},
	];
}
 
/** The confirmation each destructive-or-irreversible action asks for. */
export const PROJECT_ACTION_CONFIRM: Record<
	ProjectActionId,
	{ title: string; description: string; confirmLabel: string; destructive?: boolean }
> = {
	publish: {
		title: "Publish project",
		description:
			"This makes the project visible on the marketplace, your website and search.",
		confirmLabel: "Publish",
	},
	"move-to-draft": {
		title: "Move to draft",
		description:
			"This takes the project off the marketplace, your website and search. Nothing is deleted — you can publish it again.",
		confirmLabel: "Move to draft",
	},
	archive: {
		title: "Archive project",
		description:
			"This hides the project everywhere it is shown. You can unarchive it at any time.",
		confirmLabel: "Archive",
	},
	unarchive: {
		title: "Unarchive project",
		description:
			"This brings the project back as a draft. Publish it again when you are ready.",
		confirmLabel: "Unarchive",
	},
	delete: {
		title: "Delete project",
		description:
			"This deletes the project, its rooms and its photos. It cannot be undone.",
		confirmLabel: "Delete",
		destructive: true,
	},
};