From 4cfa76648a447e161b95552e64548fc1c958290f Mon Sep 17 00:00:00 2001 From: gaelgoth Date: Fri, 24 Jan 2025 08:24:59 +0100 Subject: [PATCH 1/4] feat: add non nullable start at column #2416 Signed-off-by: gaelgoth --- .../20250107203219_add_start_date.js | 46 ++++ .../db/seeds/02_announcements.js | 57 ++++- .../announcements-backend/src/router.test.ts | 69 +++++- .../announcements-backend/src/router.ts | 20 +- .../src/service/model.ts | 6 +- .../persistence/AnnouncementsDatabase.test.ts | 197 ++++++++++++++++++ .../persistence/AnnouncementsDatabase.ts | 24 ++- .../src/service/signal.ts | 1 + 8 files changed, 409 insertions(+), 11 deletions(-) create mode 100644 workspaces/announcements/plugins/announcements-backend/db/migrations/20250107203219_add_start_date.js diff --git a/workspaces/announcements/plugins/announcements-backend/db/migrations/20250107203219_add_start_date.js b/workspaces/announcements/plugins/announcements-backend/db/migrations/20250107203219_add_start_date.js new file mode 100644 index 0000000000..23331c8147 --- /dev/null +++ b/workspaces/announcements/plugins/announcements-backend/db/migrations/20250107203219_add_start_date.js @@ -0,0 +1,46 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.up = async function up(knex) { + // Add the new column as nullable initially + await knex.schema.alterTable('announcements', table => { + table.timestamp('start_at').nullable(); + }); + + // Backfill the start_at column with values from created_at + await knex('announcements').update({ + start_at: knex.raw('created_at'), + }); + + // Alter the column to be not nullable + await knex.schema.alterTable('announcements', table => { + table.timestamp('start_at').notNullable().alter(); + }); +}; + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = async function down(knex) { + await knex.schema.alterTable('announcements', table => { + table.dropColumn('start_at'); + }); +}; diff --git a/workspaces/announcements/plugins/announcements-backend/db/seeds/02_announcements.js b/workspaces/announcements/plugins/announcements-backend/db/seeds/02_announcements.js index b1b97576cb..9cc194cad5 100644 --- a/workspaces/announcements/plugins/announcements-backend/db/seeds/02_announcements.js +++ b/workspaces/announcements/plugins/announcements-backend/db/seeds/02_announcements.js @@ -31,6 +31,7 @@ exports.seed = async function (knex) { created_at: '2020-01-02T15:28:08.539+00:00', category: 'internal-developer-portal', active: 1, + start_at: '2025-01-01T15:00:00.539+00:00', }, { id: '1', @@ -41,6 +42,7 @@ exports.seed = async function (knex) { created_at: '2021-03-02T04:30:08.539+00:00', category: 'infrastructure', active: 1, + start_at: '2025-01-01T15:00:00.539+00:00', }, { id: '2', @@ -50,16 +52,17 @@ exports.seed = async function (knex) { body: `Today we will dive into some strategies you can use to scale Ruby on Rails applications to a huge user base. One obvious way of scaling applications is to throw more money at them. And it works amazingly well — add a few more servers, upgrade your database server, and voila, a lot of the performance issues just go poof! - + But it is often also possible to scale applications without adding more servers. That's what we will discuss today. - + Let's get going! - + Randomly taken from [this](https://blog.appsignal.com/2022/11/09/how-to-scale-ruby-on-rails-applications.html) blog post. `, category: 'infrastructure', created_at: '2021-03-17T18:28:08.539+00:00', active: 1, + start_at: '2025-01-01T15:00:00.539+00:00', }, { id: '3', @@ -69,6 +72,7 @@ exports.seed = async function (knex) { body: 'You will find the incident response metrics for the last quarter here. Our average time to resolve an incident is 2 hours. We are aiming to reduce this to 1 hour by the end of the year.', created_at: '2022-01-02T15:28:08.539+00:00', active: 1, + start_at: '2025-01-01T15:00:00.539+00:00', }, { id: '4', @@ -78,6 +82,7 @@ exports.seed = async function (knex) { body: 'We are happy to announce the new Announcements feature!', created_at: '2022-02-04T14:47:08.539+00:00', active: 1, + start_at: '2025-01-01T15:00:00.539+00:00', }, { id: '5', @@ -88,6 +93,7 @@ exports.seed = async function (knex) { created_at: '2022-03-26T01:28:08.539+00:00', category: 'internal-developer-portal', active: 1, + start_at: '2025-01-01T15:00:00.539+00:00', }, { id: '6', @@ -97,6 +103,7 @@ exports.seed = async function (knex) { body: 'We are happy to announce the new Announcements feature!', created_at: '2022-04-04T01:28:08.539+00:00', active: 1, + start_at: '2025-01-01T15:00:00.539+00:00', }, { id: '7', @@ -107,6 +114,7 @@ exports.seed = async function (knex) { category: 'javascript', created_at: '2023-02-26T01:52:01.539+00:00', active: 1, + start_at: '2025-01-01T15:00:00.539+00:00', }, { id: '8', @@ -117,6 +125,7 @@ exports.seed = async function (knex) { created_at: '2023-03-01T03:28:08.539+00:00', category: 'product-updates', active: 1, + start_at: '2025-01-01T15:00:00.539+00:00', }, { id: '9', @@ -128,6 +137,7 @@ exports.seed = async function (knex) { created_at: '2023-04-15T01:28:08.539+00:00', category: 'product-updates', active: 1, + start_at: '2025-01-01T15:00:00.539+00:00', }, { id: '10', @@ -138,6 +148,7 @@ exports.seed = async function (knex) { created_at: '2023-06-04T01:28:08.539+00:00', category: 'infrastructure', active: 1, + start_at: '2025-01-01T15:00:00.539+00:00', }, { id: '11', @@ -149,6 +160,7 @@ exports.seed = async function (knex) { created_at: '2023-06-10T08:00:00.000+00:00', category: 'infrastructure', active: 1, + start_at: '2025-01-01T15:00:00.539+00:00', }, { id: '12', @@ -159,6 +171,7 @@ exports.seed = async function (knex) { created_at: '2023-06-15T14:30:00.000+00:00', category: 'internal-developer-portal', active: 1, + start_at: '2025-01-01T15:00:00.539+00:00', }, { id: '13', @@ -168,6 +181,7 @@ exports.seed = async function (knex) { body: "We invite you to join us for an upcoming webinar on the basics of Backstage. This webinar is designed for both new and existing users who want to learn more about the features and capabilities of Backstage. Don't miss out!", created_at: '2023-06-20T10:00:00.000+00:00', active: 1, + start_at: '2025-01-01T15:00:00.539+00:00', }, { id: '14', @@ -178,6 +192,7 @@ exports.seed = async function (knex) { created_at: '2023-06-25T16:45:00.000+00:00', category: 'infrastructure', active: 1, + start_at: '2025-01-01T15:00:00.539+00:00', }, { id: '15', @@ -188,6 +203,7 @@ exports.seed = async function (knex) { created_at: '2023-06-30T09:15:00.000+00:00', category: 'security', active: 1, + start_at: '2025-01-01T15:00:00.539+00:00', }, { id: '16', @@ -198,6 +214,7 @@ exports.seed = async function (knex) { created_at: '2023-07-05T13:30:00.000+00:00', category: 'documentation', active: 1, + start_at: '2025-01-01T15:00:00.539+00:00', }, { id: '17', @@ -208,6 +225,7 @@ exports.seed = async function (knex) { created_at: '2023-07-10T07:45:00.000+00:00', category: 'infrastructure', active: 1, + start_at: '2025-01-01T15:00:00.539+00:00', }, { id: '18', @@ -218,6 +236,7 @@ exports.seed = async function (knex) { created_at: '2023-07-15T15:00:00.000+00:00', category: 'infrastructure', active: 1, + start_at: '2025-01-01T15:00:00.539+00:00', }, { id: '19', @@ -228,6 +247,7 @@ exports.seed = async function (knex) { created_at: '2023-07-20T11:30:00.000+00:00', category: 'events', active: 1, + start_at: '2025-01-01T15:00:00.539+00:00', }, { id: '20', @@ -238,6 +258,7 @@ exports.seed = async function (knex) { created_at: '2023-07-25T17:15:00.000+00:00', category: 'internal-developer-portal', active: 1, + start_at: '2025-01-01T15:00:00.539+00:00', }, { id: '21', @@ -247,6 +268,7 @@ exports.seed = async function (knex) { body: 'We will be performing a system upgrade to enhance performance and introduce new features. During this time, there may be temporary service disruptions. We apologize for any inconvenience caused and appreciate your patience.', created_at: '2023-07-30T10:45:00.000+00:00', active: 1, + start_at: '2025-01-01T15:00:00.539+00:00', }, { id: '22', @@ -256,6 +278,7 @@ exports.seed = async function (knex) { body: 'We are excited to announce the release of the Incident Analysis Dashboard. This dashboard provides detailed insights into past incidents, allowing us to analyze root causes, identify trends, and implement preventive measures. Explore the dashboard and gain valuable insights!', created_at: '2023-08-04T16:30:00.000+00:00', active: 1, + start_at: '2025-01-01T15:00:00.539+00:00', }, { id: '23', @@ -266,6 +289,7 @@ exports.seed = async function (knex) { created_at: '2023-08-09T12:00:00.000+00:00', category: 'documentation', active: 1, + start_at: '2025-01-01T15:00:00.539+00:00', }, { id: '24', @@ -276,6 +300,7 @@ exports.seed = async function (knex) { created_at: '2023-08-14T08:30:00.000+00:00', category: 'infrastructure', active: 1, + start_at: '2025-01-01T15:00:00.539+00:00', }, { id: '25', @@ -286,6 +311,7 @@ exports.seed = async function (knex) { created_at: '2023-08-19T14:45:00.000+00:00', category: 'internal-developer-portal', active: 1, + start_at: '2025-01-01T15:00:00.539+00:00', }, { id: '26', @@ -296,6 +322,7 @@ exports.seed = async function (knex) { created_at: '2023-08-24T10:15:00.000+00:00', category: 'events', active: 1, + start_at: '2025-01-01T15:00:00.539+00:00', }, { id: '27', @@ -306,6 +333,7 @@ exports.seed = async function (knex) { created_at: '2023-08-29T16:00:00.000+00:00', category: 'infrastructure', active: 1, + start_at: '2025-01-01T15:00:00.539+00:00', }, { id: '28', @@ -316,6 +344,7 @@ exports.seed = async function (knex) { created_at: '2023-09-03T09:30:00.000+00:00', category: 'infrastructure', active: 1, + start_at: '2025-01-01T15:00:00.539+00:00', }, { id: '29', @@ -326,6 +355,7 @@ exports.seed = async function (knex) { created_at: '2023-09-08T15:45:00.000+00:00', category: 'documentation', active: 1, + start_at: '2025-01-01T15:00:00.539+00:00', }, { id: '30', @@ -336,6 +366,7 @@ exports.seed = async function (knex) { created_at: '2023-09-13T11:00:00.000+00:00', category: 'infrastructure', active: 1, + start_at: '2025-01-01T15:00:00.539+00:00', }, { id: '31', @@ -346,6 +377,7 @@ exports.seed = async function (knex) { created_at: '2023-09-18T17:15:00.000+00:00', category: 'infrastructure', active: 1, + start_at: '2025-01-01T15:00:00.539+00:00', }, { id: '32', @@ -357,6 +389,7 @@ exports.seed = async function (knex) { created_at: '2023-09-23T12:30:00.000+00:00', category: 'documentation', active: 1, + start_at: '2025-01-01T15:00:00.539+00:00', }, { id: '33', @@ -367,6 +400,7 @@ exports.seed = async function (knex) { created_at: '2023-09-28T08:45:00.000+00:00', category: 'internal-developer-portal', active: 1, + start_at: '2025-01-01T15:00:00.539+00:00', }, { id: '34', @@ -377,6 +411,7 @@ exports.seed = async function (knex) { created_at: '2023-10-03T15:00:00.000+00:00', category: 'events', active: 1, + start_at: '2025-01-01T15:00:00.539+00:00', }, { id: '35', @@ -387,6 +422,7 @@ exports.seed = async function (knex) { created_at: '2023-10-08T11:15:00.000+00:00', category: 'infrastructure', active: 1, + start_at: '2025-01-01T15:00:00.539+00:00', }, { id: '36', @@ -397,6 +433,7 @@ exports.seed = async function (knex) { created_at: '2023-10-13T17:30:00.000+00:00', category: 'infrastructure', active: 1, + start_at: '2025-01-01T15:00:00.539+00:00', }, { id: '37', @@ -407,6 +444,7 @@ exports.seed = async function (knex) { created_at: '2023-10-18T13:45:00.000+00:00', category: 'documentation', active: 1, + start_at: '2025-01-01T15:00:00.539+00:00', }, { id: '38', @@ -417,6 +455,7 @@ exports.seed = async function (knex) { created_at: '2023-10-23T10:00:00.000+00:00', category: 'infrastructure', active: 1, + start_at: '2025-01-01T15:00:00.539+00:00', }, { id: '39', @@ -427,6 +466,7 @@ exports.seed = async function (knex) { created_at: '2023-10-28T16:15:00.000+00:00', category: 'infrastructure', active: 1, + start_at: '2025-01-01T15:00:00.539+00:00', }, { id: '40', @@ -438,6 +478,7 @@ exports.seed = async function (knex) { created_at: '2023-11-02T12:30:00.000+00:00', category: 'documentation', active: 1, + start_at: '2025-01-01T15:00:00.539+00:00', }, { id: '41', @@ -448,6 +489,7 @@ exports.seed = async function (knex) { created_at: '2023-11-07T09:15:00.000+00:00', category: 'infrastructure', active: 1, + start_at: '2025-01-01T15:00:00.539+00:00', }, { id: '42', @@ -458,6 +500,7 @@ exports.seed = async function (knex) { created_at: '2023-11-12T14:30:00.000+00:00', category: 'infrastructure', active: 1, + start_at: '2025-01-01T15:00:00.539+00:00', }, { id: '43', @@ -468,6 +511,7 @@ exports.seed = async function (knex) { created_at: '2023-11-17T11:45:00.000+00:00', category: 'documentation', active: 1, + start_at: '2025-01-01T15:00:00.539+00:00', }, { id: '44', @@ -478,6 +522,7 @@ exports.seed = async function (knex) { created_at: '2023-11-22T17:00:00.000+00:00', category: 'documentation', active: 1, + start_at: '2025-01-01T15:00:00.539+00:00', }, { id: '45', @@ -488,6 +533,7 @@ exports.seed = async function (knex) { created_at: '2023-11-27T13:15:00.000+00:00', category: 'events', active: 1, + start_at: '2025-01-01T15:00:00.539+00:00', }, { id: '46', @@ -498,6 +544,7 @@ exports.seed = async function (knex) { created_at: '2023-12-02T09:30:00.000+00:00', category: 'infrastructure', active: 1, + start_at: '2025-01-01T15:00:00.539+00:00', }, { id: '47', @@ -508,6 +555,7 @@ exports.seed = async function (knex) { created_at: '2023-12-07T15:45:00.000+00:00', category: 'infrastructure', active: 1, + start_at: '2025-01-01T15:00:00.539+00:00', }, { id: '48', @@ -518,6 +566,7 @@ exports.seed = async function (knex) { created_at: '2023-12-12T12:00:00.000+00:00', category: 'documentation', active: 1, + start_at: '2025-01-01T15:00:00.539+00:00', }, { id: '49', @@ -528,6 +577,7 @@ exports.seed = async function (knex) { created_at: '2023-12-17T08:15:00.000+00:00', category: 'infrastructure', active: 1, + start_at: '2025-01-01T15:00:00.539+00:00', }, { id: '50', @@ -538,6 +588,7 @@ exports.seed = async function (knex) { created_at: '2023-12-22T14:30:00.000+00:00', category: 'infrastructure', active: 1, + start_at: '2025-01-01T15:00:00.539+00:00', }, ]); }; diff --git a/workspaces/announcements/plugins/announcements-backend/src/router.test.ts b/workspaces/announcements/plugins/announcements-backend/src/router.test.ts index dda864a3b8..cc87a3a3e1 100644 --- a/workspaces/announcements/plugins/announcements-backend/src/router.test.ts +++ b/workspaces/announcements/plugins/announcements-backend/src/router.test.ts @@ -88,13 +88,22 @@ describe('createRouter', () => { body: 'body', publisher: 'user:default/name', created_at: DateTime.fromISO('2022-11-02T15:28:08.539Z'), + start_at: DateTime.fromISO('2022-11-02T15:28:08.539Z'), }, ]); const response = await request(app).get('/announcements'); expect(response.status).toEqual(200); - expect(announcementsMock).toHaveBeenCalledWith({}); + expect(announcementsMock).toHaveBeenCalledWith({ + category: undefined, + max: undefined, + offset: undefined, + active: undefined, + sortBy: 'created_at', // Default sortBy + order: 'desc', // Default order + }); + expect(response.body).toEqual([ { id: 'uuid', @@ -103,6 +112,64 @@ describe('createRouter', () => { body: 'body', publisher: 'user:default/name', created_at: '2022-11-02T15:28:08.539+00:00', + start_at: '2022-11-02T15:28:08.539+00:00', + }, + ]); + }); + it('supports sortby and order parameters', async () => { + announcementsMock.mockReturnValueOnce([ + { + id: 'uuid1', + title: 'title1', + excerpt: 'excerpt1', + body: 'body1', + publisher: 'user:default/name', + created_at: DateTime.fromISO('2025-01-01T15:28:08.539Z'), + start_at: DateTime.fromISO('2025-01-01T15:28:08.539Z'), + }, + { + id: 'uuid2', + title: 'title2', + excerpt: 'excerpt2', + body: 'body2', + publisher: 'user:default/name', + created_at: DateTime.fromISO('2025-01-02T15:28:08.539Z'), + start_at: DateTime.fromISO('2025-01-02T15:28:08.539Z'), + }, + ]); + + const response = await request(app).get( + '/announcements?sortby=created_at&order=asc', + ); + + expect(response.status).toEqual(200); + expect(announcementsMock).toHaveBeenCalledWith({ + category: undefined, + max: undefined, + offset: undefined, + active: undefined, + sortBy: 'created_at', + order: 'asc', + }); + + expect(response.body).toEqual([ + { + id: 'uuid1', + title: 'title1', + excerpt: 'excerpt1', + body: 'body1', + publisher: 'user:default/name', + created_at: '2025-01-01T15:28:08.539+00:00', + start_at: '2025-01-01T15:28:08.539+00:00', + }, + { + id: 'uuid2', + title: 'title2', + excerpt: 'excerpt2', + body: 'body2', + publisher: 'user:default/name', + created_at: '2025-01-02T15:28:08.539+00:00', + start_at: '2025-01-02T15:28:08.539+00:00', }, ]); }); diff --git a/workspaces/announcements/plugins/announcements-backend/src/router.ts b/workspaces/announcements/plugins/announcements-backend/src/router.ts index 2f914cbad7..087fff27d0 100644 --- a/workspaces/announcements/plugins/announcements-backend/src/router.ts +++ b/workspaces/announcements/plugins/announcements-backend/src/router.ts @@ -47,6 +47,7 @@ interface AnnouncementRequest { excerpt: string; body: string; active: boolean; + start_at: string; } interface CategoryRequest { @@ -58,6 +59,8 @@ type GetAnnouncementsQueryParams = { page?: number; max?: number; active?: boolean; + sortby?: 'created_at' | 'start_at'; + order?: 'asc' | 'desc'; }; export async function createRouter( @@ -100,7 +103,14 @@ export async function createRouter( '/announcements', async (req: Request<{}, {}, {}, GetAnnouncementsQueryParams>, res) => { const { - query: { category, max, page, active }, + query: { + category, + max, + page, + active, + sortby = 'created_at', + order = 'desc', + }, } = req; const results = await persistenceContext.announcementsStore.announcements( @@ -109,6 +119,10 @@ export async function createRouter( max, offset: page ? (page - 1) * (max ?? 10) : undefined, active, + sortBy: ['created_at', 'start_at'].includes(sortby) + ? sortby + : 'created_at', + order: ['asc', 'desc'].includes(order) ? order : 'desc', }, ); @@ -176,6 +190,7 @@ export async function createRouter( ...{ id: uuid(), created_at: DateTime.now(), + start_at: DateTime.fromISO(req.body.start_at), }, }); @@ -204,7 +219,7 @@ export async function createRouter( const { params: { id }, - body: { title, excerpt, body, publisher, category, active }, + body: { title, excerpt, body, publisher, category, active, start_at }, } = req; const initialAnnouncement = @@ -223,6 +238,7 @@ export async function createRouter( publisher, category, active, + start_at: DateTime.fromISO(start_at), }, }); diff --git a/workspaces/announcements/plugins/announcements-backend/src/service/model.ts b/workspaces/announcements/plugins/announcements-backend/src/service/model.ts index 25daea55a4..5f1565bd54 100644 --- a/workspaces/announcements/plugins/announcements-backend/src/service/model.ts +++ b/workspaces/announcements/plugins/announcements-backend/src/service/model.ts @@ -21,6 +21,10 @@ import { DateTime } from 'luxon'; * * @internal */ -export type AnnouncementModel = Omit & { +export type AnnouncementModel = Omit< + Announcement, + 'created_at' | 'start_at' +> & { created_at: DateTime; + start_at: DateTime; }; diff --git a/workspaces/announcements/plugins/announcements-backend/src/service/persistence/AnnouncementsDatabase.test.ts b/workspaces/announcements/plugins/announcements-backend/src/service/persistence/AnnouncementsDatabase.test.ts index 0f8f1b6424..f416d90c57 100644 --- a/workspaces/announcements/plugins/announcements-backend/src/service/persistence/AnnouncementsDatabase.test.ts +++ b/workspaces/announcements/plugins/announcements-backend/src/service/persistence/AnnouncementsDatabase.test.ts @@ -64,6 +64,7 @@ describe('AnnouncementsDatabase', () => { body: 'body', created_at: DateTime.fromISO('2023-10-26T15:28:08.539Z'), active: true, + start_at: DateTime.fromISO('2025-01-18T13:00:00.708Z'), }); const announcement = await store.announcementByID('id'); @@ -77,6 +78,7 @@ describe('AnnouncementsDatabase', () => { category: undefined, created_at: timestampToDateTime('2023-10-26T15:28:08.539Z'), active: 1, + start_at: timestampToDateTime('2025-01-18T13:00:00.708Z'), }); }); @@ -89,6 +91,7 @@ describe('AnnouncementsDatabase', () => { body: 'body', created_at: DateTime.fromISO('2023-10-26T15:28:08.539Z'), active: true, + start_at: DateTime.fromISO('2025-01-18T13:00:00.708Z'), }); const announcements = await store.announcements({}); @@ -105,6 +108,7 @@ describe('AnnouncementsDatabase', () => { category: undefined, created_at: timestampToDateTime('2023-10-26T15:28:08.539Z'), active: 1, + start_at: timestampToDateTime('2025-01-18T13:00:00.708Z'), }, ], }); @@ -119,6 +123,7 @@ describe('AnnouncementsDatabase', () => { body: 'body', created_at: DateTime.fromISO('2023-10-26T15:28:08.539Z'), active: true, + start_at: DateTime.fromISO('2025-01-18T13:00:00.708Z'), }); await store.updateAnnouncement({ @@ -129,6 +134,7 @@ describe('AnnouncementsDatabase', () => { body: 'body2', created_at: DateTime.fromISO('2023-10-26T15:28:08.539Z'), active: false, + start_at: DateTime.fromISO('2025-02-01T13:00:00.708Z'), }); const announcements = await store.announcements({}); @@ -145,6 +151,7 @@ describe('AnnouncementsDatabase', () => { category: undefined, created_at: timestampToDateTime('2023-10-26T15:28:08.539Z'), active: 0, + start_at: timestampToDateTime('2025-02-01T13:00:00.708Z'), }, ], }); @@ -159,6 +166,7 @@ describe('AnnouncementsDatabase', () => { body: 'body', created_at: DateTime.fromISO('2023-10-26T15:28:08.539Z'), active: true, + start_at: DateTime.fromISO('2025-01-18T13:00:00.708Z'), }); expect((await store.announcements({})).count).toBe(1); @@ -202,6 +210,7 @@ describe('AnnouncementsDatabase', () => { category: 'category', created_at: DateTime.fromISO('2023-10-26T15:28:08.539Z'), active: true, + start_at: DateTime.fromISO('2025-01-18T13:00:00.708Z'), }); await store.insertAnnouncement({ @@ -213,6 +222,7 @@ describe('AnnouncementsDatabase', () => { category: 'category', created_at: DateTime.fromISO('2023-10-26T15:28:08.539Z'), active: true, + start_at: DateTime.fromISO('2025-01-18T13:00:00.708Z'), }); await store.insertAnnouncement({ @@ -224,6 +234,7 @@ describe('AnnouncementsDatabase', () => { category: 'different', created_at: DateTime.fromISO('2023-10-26T15:28:08.539Z'), active: true, + start_at: DateTime.fromISO('2025-01-18T13:00:00.708Z'), }); const announcements = await store.announcements({ @@ -245,6 +256,7 @@ describe('AnnouncementsDatabase', () => { }, created_at: timestampToDateTime('2023-10-26T15:28:08.539Z'), active: 1, + start_at: timestampToDateTime('2025-01-18T13:00:00.708Z'), }, { id: 'id', @@ -258,6 +270,7 @@ describe('AnnouncementsDatabase', () => { }, created_at: timestampToDateTime('2023-10-26T15:28:08.539Z'), active: 1, + start_at: timestampToDateTime('2025-01-18T13:00:00.708Z'), }, ], }); @@ -272,6 +285,7 @@ describe('AnnouncementsDatabase', () => { body: 'body', created_at: DateTime.fromISO('2023-10-26T15:28:08.539Z'), active: true, + start_at: DateTime.fromISO('2025-01-18T13:00:00.708Z'), }); await store.insertAnnouncement({ @@ -282,6 +296,7 @@ describe('AnnouncementsDatabase', () => { body: 'body2', created_at: DateTime.fromISO('2023-10-26T15:28:09.539Z'), active: true, + start_at: DateTime.fromISO('2025-01-18T13:00:00.708Z'), }); const announcements = await store.announcements({ @@ -300,6 +315,7 @@ describe('AnnouncementsDatabase', () => { category: undefined, created_at: timestampToDateTime('2023-10-26T15:28:08.539Z'), active: 1, + start_at: timestampToDateTime('2025-01-18T13:00:00.708Z'), }, ], }); @@ -314,6 +330,7 @@ describe('AnnouncementsDatabase', () => { body: 'body', created_at: DateTime.fromISO('2023-10-26T15:28:08.539Z'), active: true, + start_at: DateTime.fromISO('2025-01-18T13:00:00.708Z'), }); await store.insertAnnouncement({ @@ -324,6 +341,7 @@ describe('AnnouncementsDatabase', () => { body: 'body2', created_at: DateTime.fromISO('2023-10-26T15:28:08.539Z'), active: true, + start_at: DateTime.fromISO('2025-01-18T13:00:00.708Z'), }); await store.insertAnnouncement({ @@ -334,6 +352,7 @@ describe('AnnouncementsDatabase', () => { body: 'body3', created_at: DateTime.fromISO('2023-10-26T15:28:08.539Z'), active: true, + start_at: DateTime.fromISO('2025-01-18T13:00:00.708Z'), }); await store.insertAnnouncement({ @@ -344,6 +363,7 @@ describe('AnnouncementsDatabase', () => { body: 'body4', created_at: DateTime.fromISO('2023-10-26T15:28:08.539Z'), active: true, + start_at: DateTime.fromISO('2025-01-18T13:00:00.708Z'), }); const announcements = await store.announcements({ @@ -362,6 +382,7 @@ describe('AnnouncementsDatabase', () => { category: undefined, created_at: timestampToDateTime('2023-10-26T15:28:08.539Z'), active: 1, + start_at: timestampToDateTime('2025-01-18T13:00:00.708Z'), }, ], }); @@ -376,6 +397,7 @@ describe('AnnouncementsDatabase', () => { body: 'body', created_at: DateTime.fromISO('2023-10-26T15:28:08.539Z'), active: false, + start_at: DateTime.fromISO('2025-01-18T13:00:00.708Z'), }); await store.insertAnnouncement({ @@ -386,6 +408,7 @@ describe('AnnouncementsDatabase', () => { body: 'body2', created_at: DateTime.fromISO('2023-10-26T15:28:08.539Z'), active: true, + start_at: DateTime.fromISO('2025-01-18T13:00:00.708Z'), }); await store.insertAnnouncement({ @@ -396,6 +419,7 @@ describe('AnnouncementsDatabase', () => { body: 'body3', created_at: DateTime.fromISO('2023-10-26T15:28:08.539Z'), active: false, + start_at: DateTime.fromISO('2025-01-18T13:00:00.708Z'), }); await store.insertAnnouncement({ @@ -406,6 +430,7 @@ describe('AnnouncementsDatabase', () => { body: 'body4', created_at: DateTime.fromISO('2023-10-26T15:28:08.539Z'), active: true, + start_at: DateTime.fromISO('2025-01-18T13:00:00.708Z'), }); const announcements = await store.announcements({ @@ -424,6 +449,7 @@ describe('AnnouncementsDatabase', () => { category: undefined, created_at: timestampToDateTime('2023-10-26T15:28:08.539Z'), active: 1, + start_at: timestampToDateTime('2025-01-18T13:00:00.708Z'), }, { id: 'id2', @@ -434,6 +460,177 @@ describe('AnnouncementsDatabase', () => { category: undefined, created_at: timestampToDateTime('2023-10-26T15:28:08.539Z'), active: 1, + start_at: timestampToDateTime('2025-01-18T13:00:00.708Z'), + }, + ], + }); + }); + it('sortBy start_at desc', async () => { + await store.insertAnnouncement({ + id: 'id1', + publisher: 'publisher1', + title: 'title1', + excerpt: 'excerpt1', + body: 'body1', + created_at: DateTime.fromISO('2023-10-26T15:28:08.539Z'), + active: true, + start_at: DateTime.fromISO('2025-01-18T13:00:00.708Z'), + }); + + await store.insertAnnouncement({ + id: 'id2', + publisher: 'publisher2', + title: 'title2', + excerpt: 'excerpt2', + body: 'body2', + created_at: DateTime.fromISO('2023-10-27T15:28:08.539Z'), + active: true, + start_at: DateTime.fromISO('2025-01-19T13:00:00.708Z'), + }); + + const announcements = await store.announcements({ + sortBy: 'start_at', + order: 'desc', + }); + + expect(announcements).toEqual({ + count: 2, + results: [ + { + id: 'id2', + publisher: 'publisher2', + title: 'title2', + excerpt: 'excerpt2', + body: 'body2', + category: undefined, + created_at: timestampToDateTime('2023-10-27T15:28:08.539Z'), + active: 1, + start_at: timestampToDateTime('2025-01-19T13:00:00.708Z'), + }, + { + id: 'id1', + publisher: 'publisher1', + title: 'title1', + excerpt: 'excerpt1', + body: 'body1', + category: undefined, + created_at: timestampToDateTime('2023-10-26T15:28:08.539Z'), + active: 1, + start_at: timestampToDateTime('2025-01-18T13:00:00.708Z'), + }, + ], + }); + }); + + it('sortBy start_at asc', async () => { + await store.insertAnnouncement({ + id: 'id1', + publisher: 'publisher1', + title: 'title1', + excerpt: 'excerpt1', + body: 'body1', + created_at: DateTime.fromISO('2023-10-26T15:28:08.539Z'), + active: true, + start_at: DateTime.fromISO('2025-01-18T13:00:00.708Z'), + }); + + await store.insertAnnouncement({ + id: 'id2', + publisher: 'publisher2', + title: 'title2', + excerpt: 'excerpt2', + body: 'body2', + created_at: DateTime.fromISO('2023-10-27T15:28:08.539Z'), + active: true, + start_at: DateTime.fromISO('2025-01-19T13:00:00.708Z'), + }); + + const announcements = await store.announcements({ + sortBy: 'start_at', + order: 'asc', + }); + + expect(announcements).toEqual({ + count: 2, + results: [ + { + id: 'id1', + publisher: 'publisher1', + title: 'title1', + excerpt: 'excerpt1', + body: 'body1', + category: undefined, + created_at: timestampToDateTime('2023-10-26T15:28:08.539Z'), + active: 1, + start_at: timestampToDateTime('2025-01-18T13:00:00.708Z'), + }, + { + id: 'id2', + publisher: 'publisher2', + title: 'title2', + excerpt: 'excerpt2', + body: 'body2', + category: undefined, + created_at: timestampToDateTime('2023-10-27T15:28:08.539Z'), + active: 1, + start_at: timestampToDateTime('2025-01-19T13:00:00.708Z'), + }, + ], + }); + }); + + it('sortBy created_at desc', async () => { + await store.insertAnnouncement({ + id: 'id1', + publisher: 'publisher1', + title: 'title1', + excerpt: 'excerpt1', + body: 'body1', + created_at: DateTime.fromISO('2023-10-25T15:28:08.539Z'), + active: true, + start_at: DateTime.fromISO('2025-01-17T13:00:00.708Z'), + }); + + await store.insertAnnouncement({ + id: 'id2', + publisher: 'publisher2', + title: 'title2', + excerpt: 'excerpt2', + body: 'body2', + created_at: DateTime.fromISO('2023-10-26T15:28:08.539Z'), + active: true, + start_at: DateTime.fromISO('2025-01-18T13:00:00.708Z'), + }); + + const announcements = await store.announcements({ + sortBy: 'created_at', + order: 'desc', + }); + + expect(announcements).toEqual({ + count: 2, + results: [ + { + id: 'id2', + publisher: 'publisher2', + title: 'title2', + excerpt: 'excerpt2', + body: 'body2', + category: undefined, + created_at: timestampToDateTime('2023-10-26T15:28:08.539Z'), + active: 1, + start_at: timestampToDateTime('2025-01-18T13:00:00.708Z'), + }, + { + id: 'id1', + publisher: 'publisher1', + title: 'title1', + excerpt: 'excerpt1', + body: 'body1', + category: undefined, + created_at: timestampToDateTime('2023-10-25T15:28:08.539Z'), + active: 1, + start_at: timestampToDateTime('2025-01-17T13:00:00.708Z'), }, ], }); diff --git a/workspaces/announcements/plugins/announcements-backend/src/service/persistence/AnnouncementsDatabase.ts b/workspaces/announcements/plugins/announcements-backend/src/service/persistence/AnnouncementsDatabase.ts index 9154db16e7..cfe32f4ebb 100644 --- a/workspaces/announcements/plugins/announcements-backend/src/service/persistence/AnnouncementsDatabase.ts +++ b/workspaces/announcements/plugins/announcements-backend/src/service/persistence/AnnouncementsDatabase.ts @@ -27,16 +27,21 @@ const announcementsTable = 'announcements'; /** * @internal */ -type AnnouncementUpsert = Omit & { +type AnnouncementUpsert = Omit< + Announcement, + 'category' | 'created_at' | 'start_at' +> & { category?: string; created_at: DateTime; + start_at: DateTime; }; /** * @internal */ -type DbAnnouncement = Omit & { +type DbAnnouncement = Omit & { category?: string; + start_at: string; }; /** @@ -89,6 +94,7 @@ const announcementUpsertToDB = ( publisher: announcement.publisher, created_at: announcement.created_at.toSQL()!, active: announcement.active, + start_at: announcement.start_at.toSQL()!, }; }; @@ -113,6 +119,7 @@ const DBToAnnouncementWithCategory = ( publisher: announcementDb.publisher, created_at: timestampToDateTime(announcementDb.created_at), active: announcementDb.active, + start_at: timestampToDateTime(announcementDb.start_at), }; }; @@ -126,7 +133,14 @@ export class AnnouncementsDatabase { async announcements( request: AnnouncementsFilters, ): Promise { - const { category, offset, max, active } = request; + const { + category, + offset, + max, + active, + sortBy = 'created_at', + order = 'desc', + } = request; // Filter the query by states // Used for both the result query and the count query @@ -164,8 +178,9 @@ export class AnnouncementsDatabase { 'created_at', 'categories.title as category_title', 'active', + 'start_at', ) - .orderBy('created_at', 'desc') + .orderBy(sortBy, order) .leftJoin('categories', 'announcements.category', 'categories.slug'); filterState(queryBuilder); filterRange(queryBuilder); @@ -202,6 +217,7 @@ export class AnnouncementsDatabase { 'created_at', 'categories.title as category_title', 'active', + 'start_at', ) .leftJoin('categories', 'announcements.category', 'categories.slug') .where('id', id) diff --git a/workspaces/announcements/plugins/announcements-backend/src/service/signal.ts b/workspaces/announcements/plugins/announcements-backend/src/service/signal.ts index 0b45390981..117f96da90 100644 --- a/workspaces/announcements/plugins/announcements-backend/src/service/signal.ts +++ b/workspaces/announcements/plugins/announcements-backend/src/service/signal.ts @@ -47,6 +47,7 @@ export const signalAnnouncement = async ( data: { ...announcement, created_at: announcement.created_at.toString(), + start_at: announcement.start_at.toString(), }, }, }); From 7692a02d160f5737d2a04c5ca84c3ca2144518d6 Mon Sep 17 00:00:00 2001 From: gaelgoth Date: Fri, 24 Jan 2025 08:27:43 +0100 Subject: [PATCH 2/4] feat: add props to set 'orderBy' and 'order' #2416 Signed-off-by: gaelgoth --- .../plugins/announcements/package.json | 2 + .../plugins/announcements/report.api.md | 8 + .../plugins/announcements/src/api.ts | 10 + .../AnnouncementsContent.tsx | 51 +++-- .../AnnouncementForm/AnnouncementForm.tsx | 189 +++++++++++------- .../AnnouncementForm/CategoryInput.test.tsx | 2 + .../AnnouncementForm/CategoryInput.tsx | 2 + .../AnnouncementsCard/AnnouncementsCard.tsx | 86 ++++---- .../AnnouncementsPage/AnnouncementsPage.tsx | 19 ++ .../AnnouncementsTimeline.test.tsx | 2 + .../AnnouncementsTimeline.tsx | 26 +++ workspaces/announcements/yarn.lock | 15 +- 12 files changed, 294 insertions(+), 118 deletions(-) diff --git a/workspaces/announcements/plugins/announcements/package.json b/workspaces/announcements/plugins/announcements/package.json index 72200b6cf8..5c4a972d5e 100644 --- a/workspaces/announcements/plugins/announcements/package.json +++ b/workspaces/announcements/plugins/announcements/package.json @@ -64,9 +64,11 @@ "@backstage/plugin-search-react": "^1.8.4", "@backstage/plugin-signals-react": "^0.0.8", "@backstage/theme": "^0.6.3", + "@date-io/luxon": "1.x", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.11.3", "@material-ui/lab": "4.0.0-alpha.57", + "@material-ui/pickers": "^3.3.11", "@mui/material": "^5.15.6", "@types/react": "^16.13.1 || ^17.0.0 || ^18.0.0", "@uiw/react-md-editor": "^4.0.3", diff --git a/workspaces/announcements/plugins/announcements/report.api.md b/workspaces/announcements/plugins/announcements/report.api.md index 7c5bd0e1a2..431e182560 100644 --- a/workspaces/announcements/plugins/announcements/report.api.md +++ b/workspaces/announcements/plugins/announcements/report.api.md @@ -39,12 +39,16 @@ export const AnnouncementsCard: ({ category, active, variant, + sortBy, + order, }: { title?: string | undefined; max?: number | undefined; category?: string | undefined; active?: boolean | undefined; variant?: InfoCardVariants | undefined; + sortBy?: 'created_at' | 'start_at' | undefined; + order?: 'desc' | 'asc' | undefined; }) => JSX_2.Element; // @public (undocumented) @@ -97,6 +101,8 @@ export const AnnouncementsTimeline: ({ timelineAlignment, timelineMinWidth, hideInactive, + sortBy, + order, }: AnnouncementsTimelineProps) => JSX_2.Element; // @public @@ -105,6 +111,8 @@ export type AnnouncementsTimelineProps = { timelineAlignment?: 'left' | 'right' | 'alternate'; timelineMinWidth?: string; hideInactive?: boolean; + sortBy?: 'created_at' | 'start_at'; + order?: 'asc' | 'desc'; }; // @public (undocumented) diff --git a/workspaces/announcements/plugins/announcements/src/api.ts b/workspaces/announcements/plugins/announcements/src/api.ts index ed1106855b..03a4622d1f 100644 --- a/workspaces/announcements/plugins/announcements/src/api.ts +++ b/workspaces/announcements/plugins/announcements/src/api.ts @@ -104,11 +104,15 @@ export class AnnouncementsClient implements AnnouncementsApi { page, category, active, + sortBy, + order, }: { max?: number; page?: number; category?: string; active?: boolean; + sortBy?: 'created_at' | 'start_at'; + order?: 'asc' | 'desc'; }): Promise { const params = new URLSearchParams(); if (category) { @@ -123,6 +127,12 @@ export class AnnouncementsClient implements AnnouncementsApi { if (active) { params.append('active', active.toString()); } + if (sortBy) { + params.append('sortby', sortBy.toString()); + } + if (order) { + params.append('order', order.toString()); + } return this.fetch(`/announcements?${params.toString()}`); } diff --git a/workspaces/announcements/plugins/announcements/src/components/Admin/AnnouncementsContent/AnnouncementsContent.tsx b/workspaces/announcements/plugins/announcements/src/components/Admin/AnnouncementsContent/AnnouncementsContent.tsx index 6d0bc8ffa1..0de2723a13 100644 --- a/workspaces/announcements/plugins/announcements/src/components/Admin/AnnouncementsContent/AnnouncementsContent.tsx +++ b/workspaces/announcements/plugins/announcements/src/components/Admin/AnnouncementsContent/AnnouncementsContent.tsx @@ -48,6 +48,7 @@ import { Button, Grid, IconButton, Typography } from '@material-ui/core'; import DeleteIcon from '@material-ui/icons/Delete'; import EditIcon from '@material-ui/icons/Edit'; import PreviewIcon from '@material-ui/icons/Visibility'; +import { DateTime } from 'luxon'; export const AnnouncementsContent = () => { const alertApi = useApi(alertApiRef); @@ -121,7 +122,7 @@ export const AnnouncementsContent = () => { const { category } = request; const slugs = categories.map((c: Category) => c.slug); - let alertMsg = t('admin.announecementsContent.alertMessage') as string; + let alertMsg = t('admin.announcementsContent.alertMessage') as string; try { if (category) { @@ -131,7 +132,7 @@ export const AnnouncementsContent = () => { if (slugs.indexOf(categorySlug) === -1) { alertMsg = alertMsg.replace('.', ''); alertMsg = `${alertMsg} ${t( - 'admin.announecementsContent.alertMessage', + 'admin.announcementsContent.alertMessage', )} ${category}.`; await announcementsApi.createCategory({ @@ -163,7 +164,7 @@ export const AnnouncementsContent = () => { const columns: TableColumn[] = [ { title: ( - {t('admin.announecementsContent.table.title')} + {t('admin.announcementsContent.table.title')} ), sorting: true, field: 'title', @@ -171,7 +172,7 @@ export const AnnouncementsContent = () => { }, { title: ( - {t('admin.announecementsContent.table.body')} + {t('admin.announcementsContent.table.body')} ), sorting: true, field: 'body', @@ -180,7 +181,7 @@ export const AnnouncementsContent = () => { { title: ( - {t('admin.announecementsContent.table.publisher')} + {t('admin.announcementsContent.table.publisher')} ), sorting: true, @@ -190,7 +191,7 @@ export const AnnouncementsContent = () => { { title: ( - {t('admin.announecementsContent.table.category')} + {t('admin.announcementsContent.table.category')} ), sorting: true, @@ -199,21 +200,43 @@ export const AnnouncementsContent = () => { }, { title: ( - {t('admin.announecementsContent.table.status')} + {t('admin.announcementsContent.table.status')} ), sorting: true, field: 'category', render: rowData => rowData.active - ? t('admin.announecementsContent.table.active') - : t('admin.announecementsContent.table.inactive'), + ? t('admin.announcementsContent.table.active') + : t('admin.announcementsContent.table.inactive'), }, { title: ( - {t('admin.announecementsContent.table.actions')} + {t('admin.announcementsContent.table.created_at')} ), + sorting: true, + field: 'created_at', + type: 'date', + render: rowData => + DateTime.fromISO(rowData.created_at).toFormat('M/d/yyyy'), + }, + { + title: ( + + {t('admin.announcementsContent.table.start_at')} + + ), + sorting: true, + field: 'start_at', + type: 'date', + render: rowData => + DateTime.fromISO(rowData.start_at).toFormat('M/d/yyyy'), + }, + { + title: ( + {t('admin.announcementsContent.table.actions')} + ), render: rowData => { return ( <> @@ -255,8 +278,8 @@ export const AnnouncementsContent = () => { onClick={() => onCreateButtonClick()} > {showCreateAnnouncementForm - ? t('admin.announecementsContent.cancelButton') - : t('admin.announecementsContent.createButton')} + ? t('admin.announcementsContent.cancelButton') + : t('admin.announcementsContent.createButton')} @@ -271,13 +294,13 @@ export const AnnouncementsContent = () => { - {t('admin.announecementsContent.noAnnouncementsFound')} + {t('admin.announcementsContent.noAnnouncementsFound')} } /> diff --git a/workspaces/announcements/plugins/announcements/src/components/AnnouncementForm/AnnouncementForm.tsx b/workspaces/announcements/plugins/announcements/src/components/AnnouncementForm/AnnouncementForm.tsx index 9c95e06e05..0c3ba173bb 100644 --- a/workspaces/announcements/plugins/announcements/src/components/AnnouncementForm/AnnouncementForm.tsx +++ b/workspaces/announcements/plugins/announcements/src/components/AnnouncementForm/AnnouncementForm.tsx @@ -24,21 +24,23 @@ import { import { Announcement } from '@backstage-community/plugin-announcements-common'; import CategoryInput from './CategoryInput'; import { - makeStyles, TextField, FormGroup, FormControlLabel, Switch, Button, + Box, + Grid, + Typography, + Divider, } from '@material-ui/core'; - -const useStyles = makeStyles(theme => ({ - formRoot: { - '& > *': { - margin: theme.spacing(1) ?? '8px', - }, - }, -})); +import { + MuiPickersUtilsProvider, + KeyboardDatePicker, +} from '@material-ui/pickers'; +import SaveAltIcon from '@material-ui/icons/SaveAlt'; +import LuxonUtils from '@date-io/luxon'; +import { DateTime } from 'luxon'; type AnnouncementFormProps = { initialData: Announcement; @@ -49,13 +51,13 @@ export const AnnouncementForm = ({ initialData, onSubmit, }: AnnouncementFormProps) => { - const classes = useStyles(); const identityApi = useApi(identityApiRef); const { t } = useAnnouncementsTranslation(); const [form, setForm] = React.useState({ ...initialData, category: initialData.category?.slug, + start_at: initialData.start_at || DateTime.now().toISO(), }); const [loading, setLoading] = useState(false); @@ -90,65 +92,116 @@ export const AnnouncementForm = ({ }; return ( - -
- - - - setForm({ ...form, ...{ body: value || '' } })} - /> - - + + + {initialData.title + ? t('announcementForm.editAnnouncement') + : t('announcementForm.newAnnouncement')} + + + + + + + + + - } - label={t('announcementForm.active')} - /> - - - + + + + + + setForm({ + ...form, + start_at: date + ? date.toISO() || '' + : DateTime.now().toISO() || '', + }) + } + KeyboardButtonProps={{ + 'aria-label': 'change date', + }} + required + /> + + + + + + + + + + setForm({ ...form, ...{ body: value || '' } }) + } + /> + + + + + + + + + + } + label={t('announcementForm.active')} + /> + + + + + +
); }; diff --git a/workspaces/announcements/plugins/announcements/src/components/AnnouncementForm/CategoryInput.test.tsx b/workspaces/announcements/plugins/announcements/src/components/AnnouncementForm/CategoryInput.test.tsx index 2e75c9d17d..c6d8336d7f 100644 --- a/workspaces/announcements/plugins/announcements/src/components/AnnouncementForm/CategoryInput.test.tsx +++ b/workspaces/announcements/plugins/announcements/src/components/AnnouncementForm/CategoryInput.test.tsx @@ -48,6 +48,7 @@ describe('CategoryInput', () => { body: string; created_at: string; active: boolean; + start_at: string; }>, ) => void = jest.fn(); @@ -60,6 +61,7 @@ describe('CategoryInput', () => { body: 'body', created_at: 'created_at', active: true, + start_at: 'start_at', }; const announcementsApiMock = { categories: jest.fn() }; diff --git a/workspaces/announcements/plugins/announcements/src/components/AnnouncementForm/CategoryInput.tsx b/workspaces/announcements/plugins/announcements/src/components/AnnouncementForm/CategoryInput.tsx index 17bc6c1d83..a590064af4 100644 --- a/workspaces/announcements/plugins/announcements/src/components/AnnouncementForm/CategoryInput.tsx +++ b/workspaces/announcements/plugins/announcements/src/components/AnnouncementForm/CategoryInput.tsx @@ -34,6 +34,7 @@ type CategoryInputProps = { body: string; created_at: string; active: boolean; + start_at: string; }>, ) => void; form: { @@ -45,6 +46,7 @@ type CategoryInputProps = { body: string; created_at: string; active: boolean; + start_at: string; }; initialValue: string; }; diff --git a/workspaces/announcements/plugins/announcements/src/components/AnnouncementsCard/AnnouncementsCard.tsx b/workspaces/announcements/plugins/announcements/src/components/AnnouncementsCard/AnnouncementsCard.tsx index bc312cc02d..1a9e609701 100644 --- a/workspaces/announcements/plugins/announcements/src/components/AnnouncementsCard/AnnouncementsCard.tsx +++ b/workspaces/announcements/plugins/announcements/src/components/AnnouncementsCard/AnnouncementsCard.tsx @@ -56,6 +56,8 @@ type AnnouncementsCardOpts = { category?: string; active?: boolean; variant?: InfoCardVariants; + sortBy?: 'created_at' | 'start_at'; + order?: 'asc' | 'desc'; }; export const AnnouncementsCard = ({ @@ -64,6 +66,8 @@ export const AnnouncementsCard = ({ category, active, variant = 'gridItem', + sortBy, + order, }: AnnouncementsCardOpts) => { const classes = useStyles(); const announcementsApi = useApi(announcementsApiRef); @@ -77,6 +81,8 @@ export const AnnouncementsCard = ({ max: max || 5, category, active, + sortBy, + order, }); const { announcementCreatePermission } = announcementEntityPermissions; @@ -104,42 +110,52 @@ export const AnnouncementsCard = ({ {announcements.results.map(announcement => ( - - {lastSeen < DateTime.fromISO(announcement.created_at) && ( - - - - )} + + + - - {announcement.title} - - } - secondary={ - <> - {DateTime.fromISO(announcement.created_at).toRelative()} - {announcement.category && ( - <> - {` ${t('announcementsCard.in')} `} - - {announcement.category.title} - - - )}{' '} - – {announcement.excerpt} - - } - /> - + + {announcement.title} + + } + secondary={ + <> + {DateTime.fromISO(announcement.created_at).toRelative()} + {announcement.category && ( + <> + {` ${t('announcementsCard.in')} `} + + {announcement.category.title} + + + )} + {' – '} + {announcement.excerpt} +
+ + {DateTime.fromISO(announcement.start_at) < DateTime.now() + ? `${t('announcementsCard.occurred')} ` + : `${t('announcementsCard.scheduled')} `} + {DateTime.fromISO(announcement.start_at).toRelative()} + + + } + />
))} {announcements.count === 0 && !loadingPermission && canAdd && ( diff --git a/workspaces/announcements/plugins/announcements/src/components/AnnouncementsPage/AnnouncementsPage.tsx b/workspaces/announcements/plugins/announcements/src/components/AnnouncementsPage/AnnouncementsPage.tsx index 10068e25f4..f751d5eab8 100644 --- a/workspaces/announcements/plugins/announcements/src/components/AnnouncementsPage/AnnouncementsPage.tsx +++ b/workspaces/announcements/plugins/announcements/src/components/AnnouncementsPage/AnnouncementsPage.tsx @@ -144,6 +144,13 @@ const AnnouncementCard = ({ )} , {DateTime.fromISO(announcement.created_at).toRelative()} +
+ + {DateTime.fromISO(announcement.start_at) < DateTime.now() + ? `${t('announcementsPage.card.occurred')} ` + : `${t('announcementsPage.card.scheduled')} `} + {DateTime.fromISO(announcement.start_at).toRelative()} + ); const { loading: loadingDeletePermission, allowed: canDelete } = @@ -225,11 +232,15 @@ const AnnouncementsGrid = ({ category, cardTitleLength, active, + sortBy, + order, }: { maxPerPage: number; category?: string; cardTitleLength?: number; active?: boolean; + sortBy?: 'created_at' | 'start_at'; + order?: 'asc' | 'desc'; }) => { const classes = useStyles(); const announcementsApi = useApi(announcementsApiRef); @@ -251,6 +262,8 @@ const AnnouncementsGrid = ({ page: page, category, active, + sortBy, + order, }, { dependencies: [maxPerPage, page, category] }, ); @@ -340,6 +353,8 @@ export type AnnouncementsPageProps = { cardOptions?: AnnouncementCardProps; hideContextMenu?: boolean; hideInactive?: boolean; + sortby?: 'created_at' | 'start_at'; + order?: 'asc' | 'desc'; }; export const AnnouncementsPage = (props: AnnouncementsPageProps) => { @@ -360,6 +375,8 @@ export const AnnouncementsPage = (props: AnnouncementsPageProps) => { maxPerPage, category, cardOptions, + sortby, + order, } = props; return ( @@ -389,6 +406,8 @@ export const AnnouncementsPage = (props: AnnouncementsPageProps) => { category={category ?? queryParams.get('category') ?? undefined} cardTitleLength={cardOptions?.titleLength} active={hideInactive ? true : false} + sortBy={sortby ?? 'created_at'} + order={order ?? 'desc'} /> diff --git a/workspaces/announcements/plugins/announcements/src/components/AnnouncementsTimeline/AnnouncementsTimeline.test.tsx b/workspaces/announcements/plugins/announcements/src/components/AnnouncementsTimeline/AnnouncementsTimeline.test.tsx index d3eca3d51e..51cbc2a03c 100644 --- a/workspaces/announcements/plugins/announcements/src/components/AnnouncementsTimeline/AnnouncementsTimeline.test.tsx +++ b/workspaces/announcements/plugins/announcements/src/components/AnnouncementsTimeline/AnnouncementsTimeline.test.tsx @@ -77,6 +77,7 @@ describe('AnnouncementsTimeline', () => { publisher: 'Publisher 1', created_at: '2022-01-01', active: true, + start_at: '2025-01-01', }, { id: '2', @@ -86,6 +87,7 @@ describe('AnnouncementsTimeline', () => { publisher: 'Publisher 2', created_at: '2022-01-02', active: true, + start_at: '2022-01-02', }, ], }; diff --git a/workspaces/announcements/plugins/announcements/src/components/AnnouncementsTimeline/AnnouncementsTimeline.tsx b/workspaces/announcements/plugins/announcements/src/components/AnnouncementsTimeline/AnnouncementsTimeline.tsx index c48006704f..8942c41777 100644 --- a/workspaces/announcements/plugins/announcements/src/components/AnnouncementsTimeline/AnnouncementsTimeline.tsx +++ b/workspaces/announcements/plugins/announcements/src/components/AnnouncementsTimeline/AnnouncementsTimeline.tsx @@ -61,6 +61,18 @@ export type AnnouncementsTimelineProps = { * Default: false */ hideInactive?: boolean; + /** + * The field by which date time to sort the announcements. + * Can be 'created_at' or 'start_at'. + * Default: 'created_at' + */ + sortBy?: 'created_at' | 'start_at'; + /** + * The order in which to sort the announcements. + * Can be 'asc' for ascending (older first) or 'desc' for descending (new first). + * Default: 'desc' + */ + order?: 'asc' | 'desc'; }; /** @@ -83,6 +95,16 @@ const DEFAULT_RESULTS_MAX = 10; */ const DEFAULT_INACTIVE = false; +/** + * Default sort by filter + */ +const DEFAULT_SORTBY = 'created_at'; + +/** + * Default order to display announcments. Newer announcements are display by default + */ +const DEFAULT_ORDER = 'desc'; + /** * Timeline of most recent announcements. * @@ -94,12 +116,16 @@ export const AnnouncementsTimeline = ({ timelineAlignment = DEFAULT_TIMELINE_ALIGNMENT, timelineMinWidth = DEFAULT_TIMELINE_WIDTH, hideInactive = DEFAULT_INACTIVE, + sortBy = DEFAULT_SORTBY, + order = DEFAULT_ORDER, }: AnnouncementsTimelineProps) => { const viewAnnouncementLink = useRouteRef(announcementViewRouteRef); const { announcements, loading, error } = useAnnouncements({ max: maxResults, active: hideInactive, + sortBy: sortBy, + order: order, }); const { t } = useAnnouncementsTranslation(); diff --git a/workspaces/announcements/yarn.lock b/workspaces/announcements/yarn.lock index 6e2cb4a397..4834cefa48 100644 --- a/workspaces/announcements/yarn.lock +++ b/workspaces/announcements/yarn.lock @@ -2738,9 +2738,11 @@ __metadata: "@backstage/plugin-signals-react": ^0.0.8 "@backstage/test-utils": ^1.7.3 "@backstage/theme": ^0.6.3 + "@date-io/luxon": 1.x "@material-ui/core": ^4.12.2 "@material-ui/icons": ^4.11.3 "@material-ui/lab": 4.0.0-alpha.57 + "@material-ui/pickers": ^3.3.11 "@mui/material": ^5.15.6 "@testing-library/jest-dom": ^6.3.0 "@testing-library/react": ^14.0.0 @@ -5271,6 +5273,17 @@ __metadata: languageName: node linkType: hard +"@date-io/luxon@npm:1.x": + version: 1.3.13 + resolution: "@date-io/luxon@npm:1.3.13" + dependencies: + "@date-io/core": ^1.3.13 + peerDependencies: + luxon: ^1.21.3 + checksum: 7dd18ef11ea14111595d8836247ffe756889bd31309796fd98f3bf05b5aaf25f63e8574280d35ae1f511ba11ba6903accc4a584b7103b8affd626b79ab69b056 + languageName: node + linkType: hard + "@davidzemon/passport-okta-oauth@npm:^0.0.5": version: 0.0.5 resolution: "@davidzemon/passport-okta-oauth@npm:0.0.5" @@ -7119,7 +7132,7 @@ __metadata: languageName: node linkType: hard -"@material-ui/pickers@npm:^3.2.10": +"@material-ui/pickers@npm:^3.2.10, @material-ui/pickers@npm:^3.3.11": version: 3.3.11 resolution: "@material-ui/pickers@npm:3.3.11" dependencies: From 9e8c3b00183ed04db04b8ba1b5923d16e965573d Mon Sep 17 00:00:00 2001 From: gaelgoth Date: Fri, 24 Jan 2025 08:29:59 +0100 Subject: [PATCH 3/4] feat: add new fields, fix typo #2416 Signed-off-by: gaelgoth --- .../announcements-common/report.api.md | 3 + .../plugins/announcements-common/src/types.ts | 6 ++ .../plugins/announcements-react/report.api.md | 72 +++++++++++-------- .../src/apis/AnnouncementsApi.ts | 2 + .../announcements-react/src/translation.ts | 9 ++- 5 files changed, 63 insertions(+), 29 deletions(-) diff --git a/workspaces/announcements/plugins/announcements-common/report.api.md b/workspaces/announcements/plugins/announcements-common/report.api.md index 5bb4f8b85e..33de2af070 100644 --- a/workspaces/announcements/plugins/announcements-common/report.api.md +++ b/workspaces/announcements/plugins/announcements-common/report.api.md @@ -15,6 +15,7 @@ export type Announcement = { body: string; created_at: string; active: boolean; + start_at: string; }; // @public @@ -37,6 +38,8 @@ export type AnnouncementsFilters = { category?: string; page?: number; active?: boolean; + sortBy?: 'created_at' | 'start_at'; + order?: 'asc' | 'desc'; }; // @public diff --git a/workspaces/announcements/plugins/announcements-common/src/types.ts b/workspaces/announcements/plugins/announcements-common/src/types.ts index 47306657a5..df798e7392 100644 --- a/workspaces/announcements/plugins/announcements-common/src/types.ts +++ b/workspaces/announcements/plugins/announcements-common/src/types.ts @@ -48,6 +48,8 @@ export type Announcement = { created_at: string; /** Whether the announcement is currently active */ active: boolean; + /** Date indicating when then announcement start */ + start_at: string; }; /** @@ -78,6 +80,10 @@ export type AnnouncementsFilters = { page?: number; /** Filter by active status */ active?: boolean; + /** Field to sort by (e.g., "created_at", "start_at") */ + sortBy?: 'created_at' | 'start_at'; + /** Sorting order: "asc" for ascending or "desc" for descending */ + order?: 'asc' | 'desc'; }; /** diff --git a/workspaces/announcements/plugins/announcements-react/report.api.md b/workspaces/announcements/plugins/announcements-react/report.api.md index ec1d8e7be2..1f44a6668e 100644 --- a/workspaces/announcements/plugins/announcements-react/report.api.md +++ b/workspaces/announcements/plugins/announcements-react/report.api.md @@ -22,6 +22,8 @@ export interface AnnouncementsApi { page?: number; category?: string; active?: boolean; + sortBy?: 'created_at' | 'start_at'; + order?: 'asc' | 'desc'; }): Promise; // (undocumented) categories(): Promise; @@ -60,20 +62,22 @@ export const announcementsTranslationRef: TranslationRef< readonly 'admin.adminPortal.subtitle': 'Manage announcements and categories'; readonly 'admin.adminPortal.announcementsLabels': 'Announcements'; readonly 'admin.adminPortal.categoriesLabel': 'Categories'; - readonly 'admin.announecementsContent.table.active': 'Active'; - readonly 'admin.announecementsContent.table.inactive': 'Inactive'; - readonly 'admin.announecementsContent.table.body': 'Body'; - readonly 'admin.announecementsContent.table.title': 'Title'; - readonly 'admin.announecementsContent.table.status': 'Status'; - readonly 'admin.announecementsContent.table.actions': 'Actions'; - readonly 'admin.announecementsContent.table.category': 'Category'; - readonly 'admin.announecementsContent.table.publisher': 'Publisher'; - readonly 'admin.announecementsContent.announcements': 'Announcements'; - readonly 'admin.announecementsContent.alertMessage': 'Announcement created.'; - readonly 'admin.announecementsContent.alertMessageWithNewCategory': 'with new category'; - readonly 'admin.announecementsContent.cancelButton': 'Cancel'; - readonly 'admin.announecementsContent.createButton': 'Create Announcement'; - readonly 'admin.announecementsContent.noAnnouncementsFound': 'No announcements found'; + readonly 'admin.announcementsContent.table.active': 'Active'; + readonly 'admin.announcementsContent.table.inactive': 'Inactive'; + readonly 'admin.announcementsContent.table.body': 'Body'; + readonly 'admin.announcementsContent.table.title': 'Title'; + readonly 'admin.announcementsContent.table.status': 'Status'; + readonly 'admin.announcementsContent.table.actions': 'Actions'; + readonly 'admin.announcementsContent.table.created_at': 'Created'; + readonly 'admin.announcementsContent.table.start_at': 'Start'; + readonly 'admin.announcementsContent.table.category': 'Category'; + readonly 'admin.announcementsContent.table.publisher': 'Publisher'; + readonly 'admin.announcementsContent.announcements': 'Announcements'; + readonly 'admin.announcementsContent.alertMessage': 'Announcement created.'; + readonly 'admin.announcementsContent.alertMessageWithNewCategory': 'with new category'; + readonly 'admin.announcementsContent.cancelButton': 'Cancel'; + readonly 'admin.announcementsContent.createButton': 'Create Announcement'; + readonly 'admin.announcementsContent.noAnnouncementsFound': 'No announcements found'; readonly 'admin.categoriesContent.table.title': 'Title'; readonly 'admin.categoriesContent.table.actions': 'Actions'; readonly 'admin.categoriesContent.table.categoryDeleted': 'Category deleted.'; @@ -90,6 +94,7 @@ export const announcementsTranslationRef: TranslationRef< readonly 'announcementForm.excerpt': 'Excerpt'; readonly 'announcementForm.editAnnouncement': 'Edit announcement'; readonly 'announcementForm.newAnnouncement': 'New announcement'; + readonly 'announcementForm.startAt': 'Announcement start date'; readonly 'announcementForm.categoryInput.label': 'Category'; readonly 'announcementForm.categoryInput.create': 'Create'; readonly 'announcementsPage.grid.announcementDeleted': 'Announcement deleted.'; @@ -101,12 +106,16 @@ export const announcementsTranslationRef: TranslationRef< readonly 'announcementsPage.card.in': 'in'; readonly 'announcementsPage.card.delete': 'DELETE'; readonly 'announcementsPage.card.edit': 'EDIT'; + readonly 'announcementsPage.card.occurred': 'Occurred '; + readonly 'announcementsPage.card.scheduled': 'Scheduled '; readonly 'deleteDialog.cancel': 'Cancel'; readonly 'deleteDialog.title': 'Are you sure you want to delete this announcement?'; readonly 'deleteDialog.delete': 'Delete'; readonly 'announcementsCard.new': 'New'; readonly 'announcementsCard.in': 'in'; readonly 'announcementsCard.announcements': 'Announcements'; + readonly 'announcementsCard.occurred': 'Occurred'; + readonly 'announcementsCard.scheduled': 'Scheduled'; readonly 'announcementsCard.seeAll': 'See all'; readonly 'announcementsCard.noAnnouncements': 'No announcements yet, want to'; readonly 'announcementsCard.addOne': 'add one'; @@ -171,20 +180,22 @@ export const useAnnouncementsTranslation: () => { readonly 'admin.adminPortal.subtitle': 'Manage announcements and categories'; readonly 'admin.adminPortal.announcementsLabels': 'Announcements'; readonly 'admin.adminPortal.categoriesLabel': 'Categories'; - readonly 'admin.announecementsContent.table.active': 'Active'; - readonly 'admin.announecementsContent.table.inactive': 'Inactive'; - readonly 'admin.announecementsContent.table.body': 'Body'; - readonly 'admin.announecementsContent.table.title': 'Title'; - readonly 'admin.announecementsContent.table.status': 'Status'; - readonly 'admin.announecementsContent.table.actions': 'Actions'; - readonly 'admin.announecementsContent.table.category': 'Category'; - readonly 'admin.announecementsContent.table.publisher': 'Publisher'; - readonly 'admin.announecementsContent.announcements': 'Announcements'; - readonly 'admin.announecementsContent.alertMessage': 'Announcement created.'; - readonly 'admin.announecementsContent.alertMessageWithNewCategory': 'with new category'; - readonly 'admin.announecementsContent.cancelButton': 'Cancel'; - readonly 'admin.announecementsContent.createButton': 'Create Announcement'; - readonly 'admin.announecementsContent.noAnnouncementsFound': 'No announcements found'; + readonly 'admin.announcementsContent.table.active': 'Active'; + readonly 'admin.announcementsContent.table.inactive': 'Inactive'; + readonly 'admin.announcementsContent.table.body': 'Body'; + readonly 'admin.announcementsContent.table.title': 'Title'; + readonly 'admin.announcementsContent.table.status': 'Status'; + readonly 'admin.announcementsContent.table.actions': 'Actions'; + readonly 'admin.announcementsContent.table.created_at': 'Created'; + readonly 'admin.announcementsContent.table.start_at': 'Start'; + readonly 'admin.announcementsContent.table.category': 'Category'; + readonly 'admin.announcementsContent.table.publisher': 'Publisher'; + readonly 'admin.announcementsContent.announcements': 'Announcements'; + readonly 'admin.announcementsContent.alertMessage': 'Announcement created.'; + readonly 'admin.announcementsContent.alertMessageWithNewCategory': 'with new category'; + readonly 'admin.announcementsContent.cancelButton': 'Cancel'; + readonly 'admin.announcementsContent.createButton': 'Create Announcement'; + readonly 'admin.announcementsContent.noAnnouncementsFound': 'No announcements found'; readonly 'admin.categoriesContent.table.title': 'Title'; readonly 'admin.categoriesContent.table.actions': 'Actions'; readonly 'admin.categoriesContent.table.categoryDeleted': 'Category deleted.'; @@ -201,6 +212,7 @@ export const useAnnouncementsTranslation: () => { readonly 'announcementForm.excerpt': 'Excerpt'; readonly 'announcementForm.editAnnouncement': 'Edit announcement'; readonly 'announcementForm.newAnnouncement': 'New announcement'; + readonly 'announcementForm.startAt': 'Announcement start date'; readonly 'announcementForm.categoryInput.label': 'Category'; readonly 'announcementForm.categoryInput.create': 'Create'; readonly 'announcementsPage.grid.announcementDeleted': 'Announcement deleted.'; @@ -212,12 +224,16 @@ export const useAnnouncementsTranslation: () => { readonly 'announcementsPage.card.in': 'in'; readonly 'announcementsPage.card.delete': 'DELETE'; readonly 'announcementsPage.card.edit': 'EDIT'; + readonly 'announcementsPage.card.occurred': 'Occurred '; + readonly 'announcementsPage.card.scheduled': 'Scheduled '; readonly 'deleteDialog.cancel': 'Cancel'; readonly 'deleteDialog.title': 'Are you sure you want to delete this announcement?'; readonly 'deleteDialog.delete': 'Delete'; readonly 'announcementsCard.new': 'New'; readonly 'announcementsCard.in': 'in'; readonly 'announcementsCard.announcements': 'Announcements'; + readonly 'announcementsCard.occurred': 'Occurred'; + readonly 'announcementsCard.scheduled': 'Scheduled'; readonly 'announcementsCard.seeAll': 'See all'; readonly 'announcementsCard.noAnnouncements': 'No announcements yet, want to'; readonly 'announcementsCard.addOne': 'add one'; diff --git a/workspaces/announcements/plugins/announcements-react/src/apis/AnnouncementsApi.ts b/workspaces/announcements/plugins/announcements-react/src/apis/AnnouncementsApi.ts index 9ef614857d..660358f7e0 100644 --- a/workspaces/announcements/plugins/announcements-react/src/apis/AnnouncementsApi.ts +++ b/workspaces/announcements/plugins/announcements-react/src/apis/AnnouncementsApi.ts @@ -40,6 +40,8 @@ export interface AnnouncementsApi { page?: number; category?: string; active?: boolean; + sortBy?: 'created_at' | 'start_at'; + order?: 'asc' | 'desc'; }): Promise; announcementByID(id: string): Promise; diff --git a/workspaces/announcements/plugins/announcements-react/src/translation.ts b/workspaces/announcements/plugins/announcements-react/src/translation.ts index 566f64449f..08d9799b1b 100644 --- a/workspaces/announcements/plugins/announcements-react/src/translation.ts +++ b/workspaces/announcements/plugins/announcements-react/src/translation.ts @@ -26,6 +26,7 @@ export const announcementsTranslationRef = createTranslationRef({ submit: 'Submit', editAnnouncement: 'Edit announcement', newAnnouncement: 'New announcement', + startAt: 'Announcement start date', categoryInput: { create: 'Create', label: 'Category', @@ -39,6 +40,8 @@ export const announcementsTranslationRef = createTranslationRef({ in: 'in', edit: 'EDIT', delete: 'DELETE', + occurred: 'Occurred ', + scheduled: 'Scheduled ', }, grid: { announcementDeleted: 'Announcement deleted.', @@ -60,6 +63,8 @@ export const announcementsTranslationRef = createTranslationRef({ in: 'in', noAnnouncements: 'No announcements yet, want to', addOne: 'add one', + occurred: 'Occurred', + scheduled: 'Scheduled', }, announcementSearchResultListItem: { published: 'Published', @@ -113,7 +118,7 @@ export const announcementsTranslationRef = createTranslationRef({ title: 'Admin Portal for Announcements', subtitle: 'Manage announcements and categories', }, - announecementsContent: { + announcementsContent: { alertMessage: 'Announcement created.', alertMessageWithNewCategory: 'with new category', cancelButton: 'Cancel', @@ -129,6 +134,8 @@ export const announcementsTranslationRef = createTranslationRef({ actions: 'Actions', active: 'Active', inactive: 'Inactive', + created_at: 'Created', + start_at: 'Start', }, }, categoriesContent: { From 547e2a9e1a9feb62b63886ed447d7e725067ed9e Mon Sep 17 00:00:00 2001 From: gaelgoth Date: Fri, 24 Jan 2025 09:14:02 +0100 Subject: [PATCH 4/4] add changeset Signed-off-by: gaelgoth --- .../announcements/.changeset/flat-numbers-destroy.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 workspaces/announcements/.changeset/flat-numbers-destroy.md diff --git a/workspaces/announcements/.changeset/flat-numbers-destroy.md b/workspaces/announcements/.changeset/flat-numbers-destroy.md new file mode 100644 index 0000000000..31d7e8c135 --- /dev/null +++ b/workspaces/announcements/.changeset/flat-numbers-destroy.md @@ -0,0 +1,11 @@ +--- +'@backstage-community/plugin-announcements-backend': minor +'@backstage-community/plugin-announcements-common': minor +'@backstage-community/plugin-announcements-react': minor +'@backstage-community/plugin-announcements': minor +--- + +- Added a `start_at` field to allow users to set the date when an announcement occurred. +- Announcements can now be sorted by `created_at` (default) or `start_at`, with customizable order (desc or asc). +- Updated the New Announcement form to accommodate start_at and future fields. +- Added `created_at` and `start_at` columns to the admin view table.