diff --git a/client/src/components/CardModal/CardModal.jsx b/client/src/components/CardModal/CardModal.jsx
old mode 100755
new mode 100644
index b31a1f05..7fb02b92
--- a/client/src/components/CardModal/CardModal.jsx
+++ b/client/src/components/CardModal/CardModal.jsx
@@ -2,7 +2,7 @@ import React, { useCallback, useRef, useState } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { useTranslation } from 'react-i18next';
-import { Button, Checkbox, Grid, Icon, Modal } from 'semantic-ui-react';
+import { Button, Checkbox, Grid, Icon, Modal, Input } from 'semantic-ui-react';
import { usePopup } from '../../lib/popup';
import { Markdown } from '../../lib/custom-ui';
@@ -83,6 +83,8 @@ const CardModal = React.memo(
}) => {
const [t] = useTranslation();
const [isLinkCopied, setIsLinkCopied] = useState(false);
+ const [isGithubLinkInputVisible, setIsGithubLinkInputVisible] = useState(false);
+ const [githubLink, setGithubLink] = useState('');
const isGalleryOpened = useRef(false);
@@ -178,6 +180,17 @@ const CardModal = React.memo(
onClose();
}, [onClose]);
+ const handleGithubLinkChange = useCallback((e) => {
+ setGithubLink(e.target.value);
+ }, []);
+
+ const handleGithubLinkSave = useCallback(() => {
+ onUpdate({
+ githubLink,
+ });
+ setIsGithubLinkInputVisible(false);
+ }, [githubLink, onUpdate]);
+
const AttachmentAddPopup = usePopup(AttachmentAddStep);
const BoardMembershipsPopup = usePopup(BoardMembershipsStep);
const LabelsPopup = usePopup(LabelsStep);
@@ -433,6 +446,31 @@ const CardModal = React.memo(
)}
+
+
+
+
{t('common.githubLink')}
+ {githubLink ? (
+
+ {githubLink}
+
+ ) : (
+
+ )}
+ {isGithubLinkInputVisible && (
+
+
+
+
+ )}
+
+
{
+ const defaultProps = {
+ name: 'Test Card',
+ description: 'Test Description',
+ dueDate: new Date(),
+ isDueDateCompleted: false,
+ stopwatch: { startedAt: null, total: 0 },
+ isSubscribed: false,
+ isActivitiesFetching: false,
+ isAllActivitiesFetched: false,
+ isActivitiesDetailsVisible: false,
+ isActivitiesDetailsFetching: false,
+ listId: '1',
+ boardId: '1',
+ projectId: '1',
+ users: [],
+ labels: [],
+ tasks: [],
+ attachments: [],
+ activities: [],
+ allProjectsToLists: [],
+ allBoardMemberships: [],
+ allLabels: [],
+ canEdit: true,
+ canEditCommentActivities: true,
+ canEditAllCommentActivities: true,
+ onUpdate: jest.fn(),
+ onMove: jest.fn(),
+ onTransfer: jest.fn(),
+ onDuplicate: jest.fn(),
+ onDelete: jest.fn(),
+ onUserAdd: jest.fn(),
+ onUserRemove: jest.fn(),
+ onBoardFetch: jest.fn(),
+ onLabelAdd: jest.fn(),
+ onLabelRemove: jest.fn(),
+ onLabelCreate: jest.fn(),
+ onLabelUpdate: jest.fn(),
+ onLabelMove: jest.fn(),
+ onLabelDelete: jest.fn(),
+ onTaskCreate: jest.fn(),
+ onTaskUpdate: jest.fn(),
+ onTaskMove: jest.fn(),
+ onTaskDelete: jest.fn(),
+ onAttachmentCreate: jest.fn(),
+ onAttachmentUpdate: jest.fn(),
+ onAttachmentDelete: jest.fn(),
+ onActivitiesFetch: jest.fn(),
+ onActivitiesDetailsToggle: jest.fn(),
+ onCommentActivityCreate: jest.fn(),
+ onCommentActivityUpdate: jest.fn(),
+ onCommentActivityDelete: jest.fn(),
+ onClose: jest.fn(),
+ };
+
+ it('should display the github link as a clickable link when filled in', () => {
+ const props = {
+ ...defaultProps,
+ githubLink: 'https://github.com/test/repo',
+ };
+
+ render();
+
+ const githubLink = screen.getByText('https://github.com/test/repo');
+ expect(githubLink).toBeInTheDocument();
+ expect(githubLink).toHaveAttribute('href', 'https://github.com/test/repo');
+ });
+
+ it('should display a button to add github link when not filled in', () => {
+ render();
+
+ const addButton = screen.getByText('Add Github Link');
+ expect(addButton).toBeInTheDocument();
+ });
+
+ it('should display an input field when the add github link button is clicked', () => {
+ render();
+
+ const addButton = screen.getByText('Add Github Link');
+ fireEvent.click(addButton);
+
+ const inputField = screen.getByPlaceholderText('Enter Github Link');
+ expect(inputField).toBeInTheDocument();
+ });
+
+ it('should call onUpdate with the github link when the save button is clicked', () => {
+ render();
+
+ const addButton = screen.getByText('Add Github Link');
+ fireEvent.click(addButton);
+
+ const inputField = screen.getByPlaceholderText('Enter Github Link');
+ fireEvent.change(inputField, { target: { value: 'https://github.com/test/repo' } });
+
+ const saveButton = screen.getByText('Save');
+ fireEvent.click(saveButton);
+
+ expect(defaultProps.onUpdate).toHaveBeenCalledWith({ githubLink: 'https://github.com/test/repo' });
+ });
+
+ it('should validate the github link format', () => {
+ render();
+
+ const addButton = screen.getByText('Add Github Link');
+ fireEvent.click(addButton);
+
+ const inputField = screen.getByPlaceholderText('Enter Github Link');
+ fireEvent.change(inputField, { target: { value: 'invalid-link' } });
+
+ const saveButton = screen.getByText('Save');
+ fireEvent.click(saveButton);
+
+ expect(defaultProps.onUpdate).not.toHaveBeenCalled();
+ });
+});
diff --git a/client/src/models/Task.js b/client/src/models/Task.js
old mode 100755
new mode 100644
index bb6b10be..02cd9e5e
--- a/client/src/models/Task.js
+++ b/client/src/models/Task.js
@@ -14,6 +14,7 @@ export default class extends BaseModel {
isCompleted: attr({
getDefault: () => false,
}),
+ githubLink: attr(), // New attribute for GitHub link
cardId: fk({
to: 'Card',
as: 'card',
diff --git a/client/src/utils/validator.js b/client/src/utils/validator.js
index 6d391a79..5aae28aa 100644
--- a/client/src/utils/validator.js
+++ b/client/src/utils/validator.js
@@ -6,3 +6,13 @@ export const isPassword = (string) => zxcvbn(string).score >= 2; // TODO: move t
export const isUsername = (string) =>
string.length >= 3 && string.length <= 16 && USERNAME_REGEX.test(string);
+
+export const isGithubLink = (string) => {
+ const urlPattern = new RegExp('^(https?:\\/\\/)?'+ // validate the protocol
+ '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.?)+[a-z]{2,}|'+ // validate the domain name
+ '((\\d{1,3}\\.){3}\\d{1,3}))'+ // OR validate the IP (v4) address
+ '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*'+ // validate the port and path
+ '(\\?[;&a-z\\d%_.~+=-]*)?'+ // validate the query string
+ '(\\#[-a-z\\d_]*)?$','i'); // validate the fragment locator
+ return !!urlPattern.test(string) && string.startsWith('https://github.com/');
+};
diff --git a/server/api/controllers/cards/show.js b/server/api/controllers/cards/show.js
old mode 100755
new mode 100644
index bc37eb06..b6074ab2
--- a/server/api/controllers/cards/show.js
+++ b/server/api/controllers/cards/show.js
@@ -46,12 +46,18 @@ module.exports = {
const tasks = await sails.helpers.cards.getTasks(card.id);
const attachments = await sails.helpers.cards.getAttachments(card.id);
+ // Include the githubLink attribute in the tasks
+ const tasksWithGithubLink = tasks.map((task) => ({
+ ...task,
+ githubLink: task.githubLink || null,
+ }));
+
return {
item: card,
included: {
cardMemberships,
cardLabels,
- tasks,
+ tasks: tasksWithGithubLink,
attachments,
},
};
diff --git a/server/api/controllers/cards/update.js b/server/api/controllers/cards/update.js
old mode 100755
new mode 100644
index df1c8c66..cee83a27
--- a/server/api/controllers/cards/update.js
+++ b/server/api/controllers/cards/update.js
@@ -43,6 +43,16 @@ const stopwatchValidator = (value) => {
return true;
};
+const githubLinkValidator = (value) => {
+ const urlPattern = new RegExp('^(https?:\\/\\/)?'+ // validate the protocol
+ '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.?)+[a-z]{2,}|'+ // validate the domain name
+ '((\\d{1,3}\\.){3}\\d{1,3}))'+ // OR validate the IP (v4) address
+ '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*'+ // validate the port and path
+ '(\\?[;&a-z\\d%_.~+=-]*)?'+ // validate the query string
+ '(\\#[-a-z\\d_]*)?$','i'); // validate the fragment locator
+ return !!urlPattern.test(value) && value.startsWith('https://github.com/');
+};
+
module.exports = {
inputs: {
id: {
@@ -91,6 +101,11 @@ module.exports = {
isSubscribed: {
type: 'boolean',
},
+ githubLink: {
+ type: 'string',
+ custom: githubLinkValidator,
+ allowNull: true,
+ },
},
exits: {
@@ -180,6 +195,7 @@ module.exports = {
'isDueDateCompleted',
'stopwatch',
'isSubscribed',
+ 'githubLink',
]);
card = await sails.helpers.cards.updateOne