Skip to content

Public Board implementation#1543

Open
aneillans wants to merge 2 commits into
plankanban:masterfrom
aneillans:public_boards
Open

Public Board implementation#1543
aneillans wants to merge 2 commits into
plankanban:masterfrom
aneillans:public_boards

Conversation

@aneillans
Copy link
Copy Markdown

An implementation (maybe a start of a better one perhaps?) of Public Boards.

Concious this is a commonly asked for feature, and it was something stopping me from finally getting off Trello, so spent some time this week and put this together; its not perfect, but it does work!

Copilot AI review requested due to automatic review settings February 20, 2026 21:02
@CLAassistant
Copy link
Copy Markdown

CLAassistant commented Feb 20, 2026

CLA assistant check
All committers have signed the CLA.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements an initial “Public Board” feature by adding server-side public visibility fields + a new unauthenticated read-only board snapshot endpoint, and a lightweight client route/view to render the public board and expose the share URL in board settings.

Changes:

  • Add isPublic / publicId fields to boards (DB + model) and auto-generate/clear publicId when toggling public visibility.
  • Add GET /api/public-boards/:publicId endpoint and allow it through policies.
  • Add client-side /public-boards/:publicId route, public board page, and a settings toggle + copyable URL.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 14 comments.

Show a summary per file
File Description
server/db/migrations/20260216120000_add_public_board_fields.js Adds DB columns for public board visibility and identifier.
server/config/routes.js Registers the new public board API route.
server/config/policies.js Marks the public board endpoint as publicly accessible.
server/api/models/Board.js Adds isPublic / publicId attributes to the Board model.
server/api/hooks/query-methods/models/Comment.js Adds getByCardIds for fetching comments across multiple cards.
server/api/hooks/query-methods/models/Board.js Adds getOneByPublicId lookup helper.
server/api/controllers/boards/update.js Allows updating isPublic and generates/clears publicId.
server/api/controllers/boards/public-show.js New controller returning a public snapshot of a board by publicId.
client/src/constants/Paths.js Adds the /public-boards/:publicId client path constant.
client/src/components/common/Root.jsx Registers a new client route for public boards.
client/src/components/common/PublicBoard.jsx New public board page that fetches and renders the snapshot.
client/src/components/common/PublicBoard.module.scss Styles for the public board page.
client/src/components/common/KanbanBoard.jsx New simplified read-only kanban renderer for public boards.
client/src/components/common/KanbanBoard.module.scss Styles for the simplified kanban renderer.
client/src/components/boards/BoardSettingsModal/GeneralPane/EditInformation.jsx Adds “Make board public” toggle + copy URL UI.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +1 to +6
exports.up = (knex) =>
knex.schema.alterTable('board', (table) => {
table.boolean('is_public').notNullable().defaultTo(false);
table.text('public_id');
table.unique('public_id');
});
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This migration file is missing the standard license header used by other migrations, and it doesn't follow the project's established pattern of adding a notNullable().defaultTo(...) boolean then running a second alter() step. Please align it with other recent migrations to avoid cross-DB alter-table quirks.

Suggested change
exports.up = (knex) =>
knex.schema.alterTable('board', (table) => {
table.boolean('is_public').notNullable().defaultTo(false);
table.text('public_id');
table.unique('public_id');
});
/*
* Copyright (c) Your Company.
* Licensed under the MIT License.
*/
exports.up = (knex) =>
knex.schema
.alterTable('board', (table) => {
table.boolean('is_public').notNullable().defaultTo(false);
table.text('public_id');
table.unique('public_id');
})
.then(() =>
knex.schema.alterTable('board', (table) => {
table.boolean('is_public').notNullable().defaultTo(false).alter();
}),
);

Copilot uses AI. Check for mistakes.
Comment on lines +145 to +154
isPublic: {
type: 'boolean',
defaultsTo: false,
columnName: 'is_public',
},
publicId: {
type: 'string',
allowNull: true,
columnName: 'public_id',
},
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Board model Swagger schema at the top of this file is now missing the newly added isPublic and publicId fields. Please update the @swagger components.schemas.Board docs (required/properties as appropriate) to match the model attributes being introduced here.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +7
/*
* Public board viewer
* Returns a sanitized, read-only snapshot of a board identified by `publicId`.
* Strips user-identifying information (creator names, comment authors) but preserves
* content (descriptions, comments, custom fields) to make the board useful for sharing.
*/

Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This controller file doesn't include the standard license header and doesn't define Swagger docs for the new public endpoint, unlike other controllers in this folder. Add the license block and document /public-boards/{publicId} (response shape + redaction behavior) so the API spec stays complete.

Suggested change
/*
* Public board viewer
* Returns a sanitized, read-only snapshot of a board identified by `publicId`.
* Strips user-identifying information (creator names, comment authors) but preserves
* content (descriptions, comments, custom fields) to make the board useful for sharing.
*/
/*
* This file is part of this project and is licensed.
* Use of this source code is governed by the license
* that can be found in the LICENSE file in the root directory
* of this source tree.
*/
/*
* Public board viewer
* Returns a sanitized, read-only snapshot of a board identified by `publicId`.
* Strips user-identifying information (creator names, comment authors) but preserves
* content (descriptions, comments, custom fields) to make the board useful for sharing.
*/
/**
* @swagger
* /public-boards/{publicId}:
* get:
* summary: Get a public, read-only snapshot of a board.
* description: >
* Returns a sanitized, read-only snapshot of a board identified by `publicId`.
* User-identifying information (such as board creator names and comment authors)
* is stripped, while content (descriptions, comments, custom fields, etc.) is preserved
* so the board is still useful when shared publicly.
* parameters:
* - in: path
* name: publicId
* required: true
* schema:
* type: string
* description: Public identifier of the board to fetch.
* responses:
* 200:
* description: A sanitized public board and its related entities.
* content:
* application/json:
* schema:
* type: object
* properties:
* item:
* type: object
* description: The public board object with sensitive fields redacted.
* included:
* type: object
* properties:
* labels:
* type: array
* items:
* type: object
* lists:
* type: array
* items:
* type: object
* cards:
* type: array
* items:
* type: object
* description: Cards with user-identifying fields removed.
* cardLabels:
* type: array
* items:
* type: object
* taskLists:
* type: array
* items:
* type: object
* tasks:
* type: array
* items:
* type: object
* customFieldGroups:
* type: array
* items:
* type: object
* customFields:
* type: array
* items:
* type: object
* customFieldValues:
* type: array
* items:
* type: object
* comments:
* type: array
* items:
* type: object
* description: Comments with author-identifying information removed.
* attachments:
* type: array
* items:
* type: object
* projects:
* type: array
* items:
* type: object
* 404:
* description: Board not found or not public.
*/

Copilot uses AI. Check for mistakes.
Comment on lines +66 to +69
const taskLists = await TaskList.qm.getByCardIds(cardIds);
const taskListIds = sails.helpers.utils.mapRecords(taskLists);
const tasks = await Task.qm.getByTaskListIds(taskListIds);
const attachments = await Attachment.qm.getByCardIds(cardIds);
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tasks are returned verbatim in the public response, but Task records include assigneeUserId (a user-identifying internal ID). This contradicts the controller's goal of stripping user-identifying data; sanitize tasks (e.g., null out assigneeUserId / omit it) before returning them.

Copilot uses AI. Check for mistakes.
Comment on lines +103 to +104
projects: [project],
},
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

projects: [project] returns the full Project record, which includes ownerProjectManagerId and other internal fields. For a public endpoint, return a sanitized project projection (e.g., id/name/background fields only) to avoid leaking user-identifying/internal IDs.

Copilot uses AI. Check for mistakes.
Comment on lines +101 to +103
comments: sanitizedComments,
attachments: sails.helpers.attachments.presentMany(attachments),
projects: [project],
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Public response returns attachments via attachments.presentMany(...), which (via presentOne) includes creatorUserId and generates download URLs under /attachments/:id/download/... that currently require an authenticated currentUser. For a public endpoint, either (1) filter/transform attachments to remove creatorUserId and exclude file attachments, or (2) implement a separate public-download flow authorized by publicId.

Copilot uses AI. Check for mistakes.
Comment on lines +55 to +61
cursor: pointer;
transition: all 0.2s;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);

&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cards are styled with cursor: pointer and hover effects, but the rendered card element has no click/keyboard interaction in KanbanBoard.jsx. Either add an actual interaction (e.g., open card modal) or change the styling to avoid suggesting the element is actionable.

Suggested change
cursor: pointer;
transition: all 0.2s;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
cursor: default;
transition: box-shadow 0.2s;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
&:hover {
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.08);

Copilot uses AI. Check for mistakes.
Comment on lines +16 to +33
const fetchPublicBoard = async () => {
try {
const response = await fetch(`${Config.SERVER_BASE_URL}/api/public-boards/${publicId}`);

if (!response.ok) {
throw new Error('Board not found or not public');
}

const data = await response.json();
setBoardData(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};

fetchPublicBoard();
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This effect performs a fetch and then sets state, but it doesn't abort/guard against the component unmounting or publicId changing mid-request. Add an AbortController (or an isMounted guard) to prevent setting state after unmount and to avoid race conditions when navigating between public boards.

Suggested change
const fetchPublicBoard = async () => {
try {
const response = await fetch(`${Config.SERVER_BASE_URL}/api/public-boards/${publicId}`);
if (!response.ok) {
throw new Error('Board not found or not public');
}
const data = await response.json();
setBoardData(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchPublicBoard();
const controller = new AbortController();
const fetchPublicBoard = async () => {
setLoading(true);
setError(null);
try {
const response = await fetch(
`${Config.SERVER_BASE_URL}/api/public-boards/${publicId}`,
{ signal: controller.signal }
);
if (!response.ok) {
throw new Error('Board not found or not public');
}
const data = await response.json();
if (!controller.signal.aborted) {
setBoardData(data);
}
} catch (err) {
if (err.name === 'AbortError') {
return;
}
setError(err.message);
} finally {
if (!controller.signal.aborted) {
setLoading(false);
}
}
};
fetchPublicBoard();
return () => {
controller.abort();
};

Copilot uses AI. Check for mistakes.
Comment on lines 164 to 170
'limitCardTypesToDefaultOne',
'alwaysDisplayCardCreator',
'expandTaskListsByDefault',
'isPublic',
);
}
if (isBoardMember) {
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this controller, a few lines below these changes there is throw Errors.NOT_ENOUGH_RIGHTS, but NOT_ENOUGH_RIGHTS is not defined in the local Errors object. That path will crash the request with an undefined property error instead of returning a proper response; define the error/exit or change to an existing error used in this controller.

Copilot uses AI. Check for mistakes.
Comment on lines 123 to 131
expandTaskListsByDefault: {
type: 'boolean',
},
isPublic: {
type: 'boolean',
},
isSubscribed: {
type: 'boolean',
},
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Swagger @swagger /boards/{id} patch docs above currently don't include the newly supported isPublic field (and the side-effect of generating/clearing publicId). Please update the requestBody schema and any relevant description so the API docs match this new behavior.

Copilot uses AI. Check for mistakes.
@meltyshev
Copy link
Copy Markdown
Collaborator

Hi! Thanks for your PR, and sorry for the delayed response. I'll take a closer look soon, but need to review and accept the previous PRs first.

Btw, I also like the idea of creating a separate path for public boards (/public-boards/:id). This way we can load just one board without modifying all the existing logic - which is really nice. The only potential issue is that every change in the normal board view would also need to be reflected in the public board view (if we make the public board look like the normal one), but that should be solvable by properly extracting the shared component parts.

@aneillans
Copy link
Copy Markdown
Author

@meltyshev Just following up on PR - anything I can do to assist ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants