Skip to content
Draft
5 changes: 5 additions & 0 deletions .changeset/opt-drag-clone-array-items.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@blinkk/root-cms": patch
---

Add opt+drag to clone array items in the CMS doc editor.
13 changes: 11 additions & 2 deletions docs/scripts/env-init.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,19 @@ ENV_FILE="$DOCS_DIR/.env"
PROJECT="rootjs-dev"
SECRET_NAME="docs-env"

if [[ -f "$ENV_FILE" ]]; then
if [[ -s "$ENV_FILE" ]]; then
exit 0
fi

echo ".env file not found, downloading from Google Secrets Manager..."
gcloud secrets versions access latest --secret="$SECRET_NAME" --project="$PROJECT" > "$ENV_FILE"
ENV_CONTENT="$(gcloud secrets versions access latest --secret="$SECRET_NAME" --project="$PROJECT")" || {
echo "ERROR: Failed to download .env from Google Secrets Manager." >&2
echo "Ensure gcloud is installed, you are authenticated, and have access to project '$PROJECT'." >&2
exit 1
}
if [[ -z "$ENV_CONTENT" ]]; then
echo "ERROR: Secret '$SECRET_NAME' returned empty content." >&2
exit 1
fi
echo "$ENV_CONTENT" > "$ENV_FILE"
echo "Downloaded .env file."
29 changes: 23 additions & 6 deletions packages/root-cms/signin/signin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -153,9 +153,26 @@ function loginSuccessRedirect() {
window.location.replace(redirectUrl);
}

const app = initializeApp(window.__ROOT_CTX.firebaseConfig);
const auth = getAuth(app);
window.firebase = {app, auth};
const root = document.getElementById('root')!;
root.innerHTML = '';
render(<SignIn />, root);
try {
const app = initializeApp(window.__ROOT_CTX.firebaseConfig);
const auth = getAuth(app);
window.firebase = {app, auth};
const root = document.getElementById('root')!;
root.innerHTML = '';
render(<SignIn />, root);
} catch (err) {
console.error(err);
const root = document.getElementById('root')!;
root.innerHTML = '';
render(<BootstrapError message={(err as Error).message} />, root);
}

function BootstrapError(props: {message: string}) {
return (
<div className="bootstrap">
<div className="bootstrap-warning">
Something went wrong: {props.message}
</div>
</div>
);
}
12 changes: 12 additions & 0 deletions packages/root-cms/signin/styles/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,18 @@ body.menu\:open {
padding: 0 24px;
}

.bootstrap-warning {
max-width: 520px;
padding: 12px;
border: 1px solid #fbbc04;
border-radius: 8px;
background: #fff9db;
color: #5f370e;
font-size: 14px;
line-height: 1.5;
font-weight: 400;
}

@keyframes bootstrap-loading-error {
0% {
opacity: 0;
Expand Down
41 changes: 28 additions & 13 deletions packages/root-cms/ui/components/DocEditor/DocEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -760,6 +760,7 @@ interface ArrayDuplicate {
draft: DraftDocController;
deepKey: string;
value: ArrayItemValue;
skipOpen?: boolean;
}

interface ArrayMoveUp {
Expand Down Expand Up @@ -906,7 +907,8 @@ function arrayReducer(state: ArrayFieldValue, action: ArrayAction) {
...data,
[newKey]: clonedValue,
_array: order,
_new: [...newlyAdded, newKey],
_new: action.skipOpen ? newlyAdded : [...newlyAdded, newKey],
_moved: action.skipOpen ? newKey : undefined,
};
}
case 'moveUp': {
Expand Down Expand Up @@ -1373,21 +1375,34 @@ DocEditor.ArrayField = (props: FieldProps) => {
return (
<Draggable key={key} index={i} draggableId={key}>
{(provided, snapshot) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
className={joinClassNames(
'DocEditor__ArrayField__item__wrapper',
snapshot.isDragging &&
'DocEditor__ArrayField__item__wrapper--dragging',
cutIndex === i &&
'DocEditor__ArrayField__item__wrapper--cut'
)}
>
<div
ref={provided.innerRef}
{...provided.draggableProps}
className={joinClassNames(
'DocEditor__ArrayField__item__wrapper',
snapshot.isDragging &&
'DocEditor__ArrayField__item__wrapper--dragging',
cutIndex === i &&
'DocEditor__ArrayField__item__wrapper--cut'
)}
>
<div
className="DocEditor__ArrayField__item__handle"
{...provided.dragHandleProps}
onClick={() => focusFieldHeader(props.deepKey, i)}
onClick={(e: MouseEvent) => {
if (e.altKey) {
dispatch({
type: 'duplicate',
draft,
deepKey: props.deepKey,
index: i,
value: getItemValue(i),
skipOpen: true,
});
return;
}
focusFieldHeader(props.deepKey, i);
}}
>
<IconGripVertical size={18} stroke={'1.5'} />
</div>
Expand Down
12 changes: 12 additions & 0 deletions packages/root-cms/ui/styles/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,18 @@ body.menu\:open {
padding: 0 24px;
}

.bootstrap-warning {
max-width: 520px;
padding: 12px;
border: 1px solid #fbbc04;
border-radius: 8px;
background: #fff9db;
color: #5f370e;
font-size: 14px;
line-height: 1.5;
font-weight: 400;
}

.sr-only {
position: absolute;
width: 1px;
Expand Down
76 changes: 49 additions & 27 deletions packages/root-cms/ui/ui.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,12 @@ declare global {
channel: true | false | 'to-preview' | 'from-preview';
};
/** Checks registered via the CMS plugin config. */
checks?: Array<{id: string; label: string; description?: string; collections?: string[]}>;
checks?: Array<{
id: string;
label: string;
description?: string;
collections?: string[];
}>;
};
firebase: FirebaseContextObject;
}
Expand Down Expand Up @@ -316,35 +321,52 @@ function registerDevServerRedirectShortcut() {

registerDevServerRedirectShortcut();

const app = initializeApp(window.__ROOT_CTX.firebaseConfig);
const databaseId = window.__ROOT_CTX.firebaseConfig.databaseId || '(default)';
// const db = getFirestore(app);
// NOTE(stevenle): the firestore web channel rpc sometimes has issues in
// collections with a large number of docs. Forcing long polling and disabling
// fetch streams seems to work for some people. This may cause performance
// issues however.
const db = initializeFirestore(
app,
{
experimentalForceLongPolling: true,
useFetchStreams: false,
} as any,
databaseId
);
const auth = getAuth(app);
const storage = getStorage(app);
auth.onAuthStateChanged((user) => {
if (!user) {
loginRedirect();
return;
}
window.firebase = {app, auth, db, storage, user};
try {
const app = initializeApp(window.__ROOT_CTX.firebaseConfig);
const databaseId = window.__ROOT_CTX.firebaseConfig.databaseId || '(default)';
// const db = getFirestore(app);
// NOTE(stevenle): the firestore web channel rpc sometimes has issues in
// collections with a large number of docs. Forcing long polling and disabling
// fetch streams seems to work for some people. This may cause performance
// issues however.
const db = initializeFirestore(
app,
{
experimentalForceLongPolling: true,
useFetchStreams: false,
} as any,
databaseId
);
const auth = getAuth(app);
const storage = getStorage(app);
auth.onAuthStateChanged((user) => {
if (!user) {
loginRedirect();
return;
}
window.firebase = {app, auth, db, storage, user};
const root = document.getElementById('root')!;
root.innerHTML = '';
render(<App />, root);

updateSession(user);
});
} catch (err) {
console.error(err);
const root = document.getElementById('root')!;
root.innerHTML = '';
render(<App />, root);
render(<BootstrapError message={(err as Error).message} />, root);
}

updateSession(user);
});
function BootstrapError(props: {message: string}) {
return (
<div className="bootstrap">
<div className="bootstrap-warning">
Something went wrong: {props.message}
</div>
</div>
);
}

async function updateSession(user: User) {
const idToken = await user.getIdToken();
Expand Down
Loading