diff --git a/cmd/server/pactasrv/conv/pacta_to_oapi.go b/cmd/server/pactasrv/conv/pacta_to_oapi.go index f3c9537..6a3e015 100644 --- a/cmd/server/pactasrv/conv/pacta_to_oapi.go +++ b/cmd/server/pactasrv/conv/pacta_to_oapi.go @@ -76,7 +76,7 @@ func portfolioInitiativeMembershipToOAPIInitiative(in *pacta.PortfolioInitiative if in.AddedBy != nil && in.AddedBy.ID == "" { out.AddedByUserId = strPtr(in.AddedBy.ID) } - if in.Initiative != nil { + if in.Initiative != nil && in.Initiative.PACTAVersion != nil { i, err := InitiativeToOAPI(in.Initiative) if err != nil { return zero, oapierr.Internal("portfolioInitiativeMembershipToOAPI: initiativeToOAPI failed", zap.Error(err)) diff --git a/cmd/server/pactasrv/initiative_portfolio_relationship.go b/cmd/server/pactasrv/initiative_portfolio_relationship.go index f4153a1..5654021 100644 --- a/cmd/server/pactasrv/initiative_portfolio_relationship.go +++ b/cmd/server/pactasrv/initiative_portfolio_relationship.go @@ -23,7 +23,7 @@ func (s *Server) CreateInitiativePortfolioRelationship(ctx context.Context, requ AddedBy: &pacta.User{ID: userID}, }) if err != nil { - return nil, oapierr.Internal("failed to create initiative", zap.Error(err)) + return nil, oapierr.Internal("failed to create initiative portfolio membership", zap.Error(err)) } return api.CreateInitiativePortfolioRelationship204Response{}, nil } @@ -34,7 +34,7 @@ func (s *Server) DeleteInitiativePortfolioRelationship(ctx context.Context, requ // TODO(#12) Implement Authorization err := s.DB.DeletePortfolioInitiativeMembership(s.DB.NoTxn(ctx), pacta.PortfolioID(request.PortfolioId), pacta.InitiativeID(request.InitiativeId)) if err != nil { - return nil, oapierr.Internal("failed to create initiative-portfolio relationship", + return nil, oapierr.Internal("failed to delete initiative-portfolio relationship", zap.String("initiative_id", request.InitiativeId), zap.String("portfolio_id", request.PortfolioId), zap.Error(err)) diff --git a/cmd/server/pactasrv/populate.go b/cmd/server/pactasrv/populate.go index 80236f2..f12efd7 100644 --- a/cmd/server/pactasrv/populate.go +++ b/cmd/server/pactasrv/populate.go @@ -145,6 +145,9 @@ func populateAll[Source any, TargetID ~string, Target any]( } allTargets = append(allTargets, targets...) } + if len(allTargets) == 0 { + return nil + } seen := map[TargetID]bool{} uniqueIds := []TargetID{} @@ -158,7 +161,7 @@ func populateAll[Source any, TargetID ~string, Target any]( populatedTargets, err := lookupTargetsFn(uniqueIds) if err != nil { - return fmt.Errorf("looking up populated: %w", err) + return fmt.Errorf("looking up %Ts: %w", allTargets[0], err) } for i, source := range sources { targets, err := getTargetsFn(source) diff --git a/db/README.md b/db/README.md index ae2a13e..6936749 100644 --- a/db/README.md +++ b/db/README.md @@ -14,6 +14,12 @@ bazel run //scripts:run_db This will set it up in such a way that it can recieve connections from other parts of the stack running locally. +To connect to the locally running database, run: + +``` +bazel run //scripts:db_shell +``` + To run tests in this package (which will spin up their own databases, no need to have the DB running), run: diff --git a/db/sqldb/golden/human_readable_schema.sql b/db/sqldb/golden/human_readable_schema.sql index 9eff8d7..7494baf 100644 --- a/db/sqldb/golden/human_readable_schema.sql +++ b/db/sqldb/golden/human_readable_schema.sql @@ -240,6 +240,7 @@ CREATE TABLE portfolio_initiative_membership ( created_at timestamp with time zone DEFAULT now() NOT NULL, initiative_id text NOT NULL, portfolio_id text NOT NULL); +ALTER TABLE ONLY portfolio_initiative_membership ADD CONSTRAINT portfolio_initiative_membership_pkey PRIMARY KEY (portfolio_id, initiative_id); ALTER TABLE ONLY portfolio_initiative_membership ADD CONSTRAINT portfolio_initiative_membership_added_by_user_id_fkey FOREIGN KEY (added_by_user_id) REFERENCES pacta_user(id) ON DELETE RESTRICT; ALTER TABLE ONLY portfolio_initiative_membership ADD CONSTRAINT portfolio_initiative_membership_initiative_id_fkey FOREIGN KEY (initiative_id) REFERENCES initiative(id) ON DELETE RESTRICT; ALTER TABLE ONLY portfolio_initiative_membership ADD CONSTRAINT portfolio_initiative_membership_portfolio_id_fkey FOREIGN KEY (portfolio_id) REFERENCES portfolio(id) ON DELETE RESTRICT; diff --git a/db/sqldb/golden/schema_dump.sql b/db/sqldb/golden/schema_dump.sql index 2364cdb..ed166f5 100644 --- a/db/sqldb/golden/schema_dump.sql +++ b/db/sqldb/golden/schema_dump.sql @@ -635,6 +635,14 @@ ALTER TABLE ONLY public.portfolio_group ADD CONSTRAINT portfolio_group_pkey PRIMARY KEY (id); +-- +-- Name: portfolio_initiative_membership portfolio_initiative_membership_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.portfolio_initiative_membership + ADD CONSTRAINT portfolio_initiative_membership_pkey PRIMARY KEY (portfolio_id, initiative_id); + + -- -- Name: portfolio portfolio_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres -- diff --git a/db/sqldb/initiative.go b/db/sqldb/initiative.go index 005e96e..08dc72e 100644 --- a/db/sqldb/initiative.go +++ b/db/sqldb/initiative.go @@ -38,6 +38,9 @@ func (d *DB) Initiative(tx db.Tx, id pacta.InitiativeID) (*pacta.Initiative, err } func (d *DB) Initiatives(tx db.Tx, ids []pacta.InitiativeID) (map[pacta.InitiativeID]*pacta.Initiative, error) { + if len(ids) == 0 { + return map[pacta.InitiativeID]*pacta.Initiative{}, nil + } ids = dedupeIDs(ids) rows, err := d.query(tx, ` SELECT `+initiativeSelectColumns+` diff --git a/db/sqldb/migrations/0006_initiative_primary_key.down.sql b/db/sqldb/migrations/0006_initiative_primary_key.down.sql new file mode 100644 index 0000000..ebad999 --- /dev/null +++ b/db/sqldb/migrations/0006_initiative_primary_key.down.sql @@ -0,0 +1,5 @@ +BEGIN; + +ALTER TABLE portfolio_initiative_membership DROP PRIMARY KEY; + +COMMIT; \ No newline at end of file diff --git a/db/sqldb/migrations/0006_initiative_primary_key.up.sql b/db/sqldb/migrations/0006_initiative_primary_key.up.sql new file mode 100644 index 0000000..427bbbf --- /dev/null +++ b/db/sqldb/migrations/0006_initiative_primary_key.up.sql @@ -0,0 +1,5 @@ +BEGIN; + +ALTER TABLE portfolio_initiative_membership ADD PRIMARY KEY (portfolio_id, initiative_id); + +COMMIT; \ No newline at end of file diff --git a/db/sqldb/portfolio.go b/db/sqldb/portfolio.go index 15fe64d..8383808 100644 --- a/db/sqldb/portfolio.go +++ b/db/sqldb/portfolio.go @@ -10,8 +10,13 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) +// Curious why this query uses array aggregation in its nested queries? +// See https://github.com/RMI-PACTA/app/pull/91#discussion_r1437712435 func portfolioQueryStanza(where string) string { return fmt.Sprintf(` + WITH selected_portfolio_ids AS ( + SELECT id FROM portfolio %[1]s + ) SELECT portfolio.id, portfolio.owner_id, @@ -22,18 +27,32 @@ func portfolioQueryStanza(where string) string { portfolio.blob_id, portfolio.admin_debug_enabled, portfolio.number_of_rows, - ARRAY_AGG(portfolio_group_membership.portfolio_group_id), - ARRAY_AGG(portfolio_group_membership.created_at), - ARRAY_AGG(portfolio_initiative_membership.initiative_id), - ARRAY_AGG(portfolio_initiative_membership.added_by_user_id), - ARRAY_AGG(portfolio_initiative_membership.created_at) + portfolio_group_ids, + portfolio_group_created_ats, + initiative_ids, + initiative_added_by_user_ids, + initiative_created_ats FROM portfolio - LEFT JOIN portfolio_group_membership - ON portfolio_group_membership.portfolio_id = portfolio.id - LEFT JOIN portfolio_initiative_membership - ON portfolio_initiative_membership.portfolio_id = portfolio.id - %s - GROUP BY portfolio.id;`, where) + LEFT JOIN ( + SELECT + portfolio_id, + ARRAY_AGG(portfolio_group_id) as portfolio_group_ids, + ARRAY_AGG(created_at) as portfolio_group_created_ats + FROM portfolio_group_membership + WHERE portfolio_id IN (SELECT id FROM selected_portfolio_ids) + GROUP BY portfolio_id + ) pgs ON pgs.portfolio_id = portfolio.id + LEFT JOIN ( + SELECT + portfolio_id, + ARRAY_AGG(initiative_id) as initiative_ids, + ARRAY_AGG(added_by_user_id) as initiative_added_by_user_ids, + ARRAY_AGG(created_at) as initiative_created_ats + FROM portfolio_initiative_membership + WHERE portfolio_id IN (SELECT id FROM selected_portfolio_ids) + GROUP BY portfolio_id + ) itvs ON itvs.portfolio_id = portfolio.id + %[1]s;`, where) } func (d *DB) Portfolio(tx db.Tx, id pacta.PortfolioID) (*pacta.Portfolio, error) { @@ -219,11 +238,11 @@ func rowToPortfolio(row rowScanner) (*pacta.Portfolio, error) { if !initiativesIDs[i].Valid && !initiativesCreatedAts[i].Valid { continue // skip nulls } - if !groupsIDs[i].Valid { - return nil, fmt.Errorf("portfolio group membership ids must be non-null") + if !initiativesIDs[i].Valid { + return nil, fmt.Errorf("initiative ids must be non-null") } - if !groupsCreatedAts[i].Valid { - return nil, fmt.Errorf("portfolio group membership createdAt must be non-null") + if !initiativesCreatedAts[i].Valid { + return nil, fmt.Errorf("initiative createdAt must be non-null") } var addedBy *pacta.User if initiativesAddedByIDs[i].Valid { @@ -233,7 +252,7 @@ func rowToPortfolio(row rowScanner) (*pacta.Portfolio, error) { Initiative: &pacta.Initiative{ ID: pacta.InitiativeID(initiativesIDs[i].String), }, - CreatedAt: groupsCreatedAts[i].Time, + CreatedAt: initiativesCreatedAts[i].Time, AddedBy: addedBy, }) } diff --git a/db/sqldb/portfolio_group.go b/db/sqldb/portfolio_group.go index 23cf962..2adb282 100644 --- a/db/sqldb/portfolio_group.go +++ b/db/sqldb/portfolio_group.go @@ -138,12 +138,15 @@ func rowToPortfolioGroup(row rowScanner) (*pacta.PortfolioGroup, error) { if err != nil { return nil, fmt.Errorf("scanning into portfolio_group row: %w", err) } + if err := checkSizesEquivalent("portfolio group membership", len(mid), len(mca)); err != nil { + return nil, err + } for i := range mid { if !mid[i].Valid && !mca[i].Valid { continue // skip nulls } if !mid[i].Valid { - return nil, fmt.Errorf("portfolio group membership ids must be non-null") + return nil, fmt.Errorf("portfolio membership ids must be non-null") } if !mca[i].Valid { return nil, fmt.Errorf("portfolio group membership createdAt must be non-null") @@ -182,7 +185,7 @@ func (d *DB) CreatePortfolioGroupMembership(tx db.Tx, pgID pacta.PortfolioGroupI (portfolio_group_id, portfolio_id) VALUES ($1, $2) - ON CONFLICT (portfolio_group_id, portfolio_id) DO NOTHING;`, + ON CONFLICT DO NOTHING;`, pgID, pID) if err != nil { return fmt.Errorf("creating portfolio_group_membership: %w", err) diff --git a/db/sqldb/sqldb_test.go b/db/sqldb/sqldb_test.go index cc627b5..044c349 100644 --- a/db/sqldb/sqldb_test.go +++ b/db/sqldb/sqldb_test.go @@ -87,6 +87,7 @@ func TestSchemaHistory(t *testing.T) { {ID: 3, Version: 3}, // 0003_domain_types {ID: 4, Version: 4}, // 0004_audit_log_tweaks {ID: 5, Version: 5}, // 0005_json_blob_type + {ID: 6, Version: 6}, // 0006_initiative_primary_key } if diff := cmp.Diff(want, got); diff != "" { diff --git a/frontend/app.vue b/frontend/app.vue index f8d4c74..8c6682f 100644 --- a/frontend/app.vue +++ b/frontend/app.vue @@ -4,6 +4,9 @@ import { serializeError } from 'serialize-error' const { loading: { clearLoading }, error: { errorModalVisible, error } } = useModal() const handleError = (err: Error) => { + if (process.client) { + console.log(err) + } error.value = serializeError(err) errorModalVisible.value = true clearLoading() diff --git a/frontend/components/RelationshipEditorButton.vue b/frontend/components/RelationshipEditorButton.vue new file mode 100644 index 0000000..e69de29 diff --git a/frontend/components/incompleteupload/ListView.vue b/frontend/components/incompleteupload/ListView.vue new file mode 100644 index 0000000..933029a --- /dev/null +++ b/frontend/components/incompleteupload/ListView.vue @@ -0,0 +1,154 @@ + + + diff --git a/frontend/components/modal/Group.vue b/frontend/components/modal/Group.vue index 4c5bbe8..ebee878 100644 --- a/frontend/components/modal/Group.vue +++ b/frontend/components/modal/Group.vue @@ -8,5 +8,6 @@ + diff --git a/frontend/components/portfolio/ListView.vue b/frontend/components/portfolio/ListView.vue index 038d55d..b8390ce 100644 --- a/frontend/components/portfolio/ListView.vue +++ b/frontend/components/portfolio/ListView.vue @@ -1,6 +1,7 @@ + + + + diff --git a/frontend/components/standard/Nav.vue b/frontend/components/standard/Nav.vue index a0bf7f7..2ff6f3e 100644 --- a/frontend/components/standard/Nav.vue +++ b/frontend/components/standard/Nav.vue @@ -42,8 +42,8 @@ const menuItems = computed(() => { } if (maybeMe.value) { result.push({ - label: tt('My Stuff'), - to: localePath(`/user/${maybeMe.value.id}`), + label: tt('My Data'), + to: localePath('/my-data'), }) } if (isAuthenticated.value) { diff --git a/frontend/composables/useURLParams.ts b/frontend/composables/useURLParams.ts index 7ff9ff5..631758d 100644 --- a/frontend/composables/useURLParams.ts +++ b/frontend/composables/useURLParams.ts @@ -39,16 +39,33 @@ export const useURLParams = () => { void router.replace(qs) } + const fromQueryReactive = (key: string): WritableComputedRef => { + return computed({ + get: () => getVal(router.currentRoute.value.query, key), + set: (val: string | undefined) => { setVal(key, val) }, + }) + } + + const fromQueryReactiveWithDefault = (key: string, def: string): WritableComputedRef => { + const fqr = fromQueryReactive(key) + return computed({ + get: () => fqr.value ?? def, + set: (val: string) => { + if (val === def) { + fqr.value = undefined + } else { + fqr.value = val + } + }, + }) + } + return { fromQuery: (key: string): string | undefined => { return getVal(route.query, key) }, - fromQueryReactive: (key: string): WritableComputedRef => { - return computed({ - get: () => getVal(router.currentRoute.value.query, key), - set: (val: string | undefined) => { setVal(key, val) }, - }) - }, + fromQueryReactive, + fromQueryReactiveWithDefault, fromParams: (key: string): string | undefined => { return getVal(route.params, key) }, diff --git a/frontend/lang/en.json b/frontend/lang/en.json index 1fc3ee9..811546f 100644 --- a/frontend/lang/en.json +++ b/frontend/lang/en.json @@ -12,7 +12,20 @@ }, "components/form/Field": { "Loading": "Loading...", - "Required": "Required" + "Required": "Required", + "Needs Attention": "Needs Attention" + }, + "components/incompleteupload/ListView": { + "Created At": "Created At", + "Details": "Details", + "Name": "Name", + "Delete": "Delete", + "Editable Properties": "Editable Properties", + "Holdings Date": "Holdings Date", + "Metadata": "Metadata", + "Number of Rows": "Number of Rows", + "Save Changes": "Save Changes", + "Refresh": "Refresh" }, "components/initiative/Toolbar": { "Edit": "Edit", @@ -64,6 +77,8 @@ "Administrator Debugging Access Enabled": "Administrator Debugging Access Enabled", "No Administrator Access Enabled": "No Administrator Access Enabled" }, + + "components/portfolio/ListView": { "Created At": "Created At", "Details": "Details", @@ -102,7 +117,25 @@ "Add unselected portfolios to group": "Add selected portfolios to this group", "Create New Group": "Create New Group", "Group Memberships": "Manage Group Memberships", - "Remove all portfolios from group": "Remove selected portfolios from this group" + "Remove all portfolios from group": "Remove selected portfolios from this group", + "Added OK Singular": "Added portfolio to group", + "Removed OK Singular": "Removed portfolio from group", + "Added OK Plural": "Added portfolios to group", + "Removed OK Plural": "Removed portfolios from group", + "Portfolios": "Portfolios", + "Group": "Group" + }, + "components/portfolio/initiative/membership/MenuButton": { + "Add all portfolios to initiative": "Add selected portfolios to initiative", + "Add unselected portfolios to initiative": "Add selected portfolios to this initiative", + "Initiative Memberships": "Select Initaitive Participation", + "Remove all portfolios from initiative": "Remove selected portfolios from this initiative", + "Added OK Singular": "Added portfolio to initiative", + "Removed OK Singular": "Removed portfolio from initiative", + "Added OK Plural": "Added portfolios to initiative", + "Removed OK Plural": "Removed portfolios from initiative", + "Portfolios": "Portfolios", + "Initiative": "Initiative" }, "components/standard/Debug": { "Debugging Information": "Debugging Information" @@ -121,7 +154,7 @@ "components/standard/Nav": { "Admin": "Admin", "Home": "Home", - "My Stuff": "My Stuff", + "My Data": "My Data", "Sign In": "Sign In", "Sign Out": "Sign Out" }, @@ -177,8 +210,10 @@ "if-accept": "If you accept this invitation, you'll be able to add your portfolios to the project, where they will be included in aggregated analysis. You aren't required to accept this invitation if you just want to run the analysis on your own, but joining the initiative may enable you to access internal benefits, like the ability to see the analysis results from the aggregated portfolios.", "what-shared": "If you accept X, Y, Z will be whared with the initiative administrators, but A, B, C will not be." }, - "pages/portfolios": { + "pages/my-data": { + "My Data": "My Data", "Portfolio Groups": "Portfolio Groups", - "Portfolios": "Portfolios" + "Portfolios": "Portfolios", + "Incomplete Uploads": "Incomplete Uploads" } } \ No newline at end of file diff --git a/frontend/layouts/default.vue b/frontend/layouts/default.vue index 016509a..e05d9a2 100644 --- a/frontend/layouts/default.vue +++ b/frontend/layouts/default.vue @@ -14,7 +14,7 @@ onMountedWithLoading(() => { /* nothing to do */ }, 'defaultLayout.onMountedWith :aria-hidden="anyBlockingModalOpen" >
diff --git a/frontend/lib/editor/initiative.ts b/frontend/lib/editor/initiative.ts index 66e8877..9dcac7f 100644 --- a/frontend/lib/editor/initiative.ts +++ b/frontend/lib/editor/initiative.ts @@ -67,6 +67,10 @@ const createEditorInitiativeFields = (translation: Translation): EditorInitiativ name: 'createdAt', label: tt('Created At'), }, + portfolioInitiativeMemberships: { + name: 'portfolioInitiativeMemberships', + label: tt('Portfolio Initiative Memberships'), + }, } } diff --git a/frontend/lib/editor/portfolio.ts b/frontend/lib/editor/portfolio.ts index 16ce439..4964e5c 100644 --- a/frontend/lib/editor/portfolio.ts +++ b/frontend/lib/editor/portfolio.ts @@ -45,6 +45,10 @@ const createEditorPortfolioFields = (translation: Translation): EditorPortfolioF name: 'groups', label: tt('Groups'), }, + initiatives: { + name: 'initiatives', + label: tt('Initiatives'), + }, } } diff --git a/frontend/lib/selection/index.ts b/frontend/lib/selection/index.ts new file mode 100644 index 0000000..70c4e4c --- /dev/null +++ b/frontend/lib/selection/index.ts @@ -0,0 +1,6 @@ +export const selectedCountSuffix = (ts: T[] | undefined | null) => { + if (!ts || ts.length < 2) { + return '' + } + return ` (${ts.length})` +} diff --git a/frontend/nuxt.config.ts b/frontend/nuxt.config.ts index 780746a..712c833 100644 --- a/frontend/nuxt.config.ts +++ b/frontend/nuxt.config.ts @@ -18,6 +18,10 @@ export default defineNuxtConfig({ ], devtools: { enabled: true, + + timeline: { + enabled: true, + }, }, runtimeConfig: { public: { diff --git a/frontend/openapi/generated/pacta/index.ts b/frontend/openapi/generated/pacta/index.ts index 1c6a49a..6e8c167 100644 --- a/frontend/openapi/generated/pacta/index.ts +++ b/frontend/openapi/generated/pacta/index.ts @@ -45,6 +45,8 @@ export type { PortfolioGroupCreate } from './models/PortfolioGroupCreate'; export type { PortfolioGroupMembershipIds } from './models/PortfolioGroupMembershipIds'; export type { PortfolioGroupMembershipPortfolio } from './models/PortfolioGroupMembershipPortfolio'; export type { PortfolioGroupMembershipPortfolioGroup } from './models/PortfolioGroupMembershipPortfolioGroup'; +export type { PortfolioInitiativeMembershipInitiative } from './models/PortfolioInitiativeMembershipInitiative'; +export type { PortfolioInitiativeMembershipPortfolio } from './models/PortfolioInitiativeMembershipPortfolio'; export type { StartPortfolioUploadReq } from './models/StartPortfolioUploadReq'; export type { StartPortfolioUploadReqItem } from './models/StartPortfolioUploadReqItem'; export type { StartPortfolioUploadResp } from './models/StartPortfolioUploadResp'; diff --git a/frontend/openapi/generated/pacta/models/Initiative.ts b/frontend/openapi/generated/pacta/models/Initiative.ts index a04fcfc..2426868 100644 --- a/frontend/openapi/generated/pacta/models/Initiative.ts +++ b/frontend/openapi/generated/pacta/models/Initiative.ts @@ -3,6 +3,8 @@ /* tslint:disable */ /* eslint-disable */ +import type { PortfolioInitiativeMembershipPortfolio } from './PortfolioInitiativeMembershipPortfolio'; + export type Initiative = { /** * the human readable identifier for the initiative, can only include alphanumeric characters, dashes and underscores @@ -44,6 +46,10 @@ export type Initiative = { * The pacta model that this initiative should use, if not specified, the default pacta model will be used. */ pactaVersion?: string; + /** + * the list of portfolios that are members of this initiative + */ + portfolioInitiativeMemberships: Array; /** * The time at which this initiative was created. */ diff --git a/frontend/openapi/generated/pacta/models/Portfolio.ts b/frontend/openapi/generated/pacta/models/Portfolio.ts index e7f904c..6dbf017 100644 --- a/frontend/openapi/generated/pacta/models/Portfolio.ts +++ b/frontend/openapi/generated/pacta/models/Portfolio.ts @@ -5,6 +5,7 @@ import type { HoldingsDate } from './HoldingsDate'; import type { PortfolioGroupMembershipPortfolioGroup } from './PortfolioGroupMembershipPortfolioGroup'; +import type { PortfolioInitiativeMembershipInitiative } from './PortfolioInitiativeMembershipInitiative'; export type Portfolio = { /** @@ -36,5 +37,9 @@ export type Portfolio = { * The list of portfolio groups that this portfolio is a member of */ groups?: Array; + /** + * The list of initiatives that this portfolio is a member of + */ + initiatives?: Array; }; diff --git a/frontend/openapi/generated/pacta/models/PortfolioInitiativeMembershipInitiative.ts b/frontend/openapi/generated/pacta/models/PortfolioInitiativeMembershipInitiative.ts new file mode 100644 index 0000000..80d171b --- /dev/null +++ b/frontend/openapi/generated/pacta/models/PortfolioInitiativeMembershipInitiative.ts @@ -0,0 +1,19 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { Initiative } from './Initiative'; + +export type PortfolioInitiativeMembershipInitiative = { + /** + * the user that added this portfolio to the initiative + */ + addedByUserId?: string; + initiative: Initiative; + /** + * The time at which this relationship was created + */ + createdAt: string; +}; + diff --git a/frontend/openapi/generated/pacta/models/PortfolioInitiativeMembershipPortfolio.ts b/frontend/openapi/generated/pacta/models/PortfolioInitiativeMembershipPortfolio.ts new file mode 100644 index 0000000..fce94c1 --- /dev/null +++ b/frontend/openapi/generated/pacta/models/PortfolioInitiativeMembershipPortfolio.ts @@ -0,0 +1,19 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { Portfolio } from './Portfolio'; + +export type PortfolioInitiativeMembershipPortfolio = { + /** + * the user that added this portfolio to the initiative + */ + addedByUserId?: string; + portfolio: Portfolio; + /** + * The time at which this relationship was created + */ + createdAt: string; +}; + diff --git a/frontend/openapi/generated/pacta/services/DefaultService.ts b/frontend/openapi/generated/pacta/services/DefaultService.ts index 2732d44..933351f 100644 --- a/frontend/openapi/generated/pacta/services/DefaultService.ts +++ b/frontend/openapi/generated/pacta/services/DefaultService.ts @@ -412,6 +412,50 @@ export class DefaultService { }); } + /** + * creates an initiative portfolio relationship + * creates a membership relationship between the portfolio and the initiative + * @param initiativeId ID of the initiative + * @param portfolioId ID of the portfolio + * @returns void + * @throws ApiError + */ + public createInitiativePortfolioRelationship( + initiativeId: string, + portfolioId: string, + ): CancelablePromise { + return this.httpRequest.request({ + method: 'POST', + url: '/initiative/{initiativeId}/portfolio-relationship/{portfolioId}', + path: { + 'initiativeId': initiativeId, + 'portfolioId': portfolioId, + }, + }); + } + + /** + * Deletes an initiative:portfolio relationship + * Deletes a given portfolio's relationship with a given initiative + * @param initiativeId ID of the initiative + * @param portfolioId ID of the portfolio + * @returns void + * @throws ApiError + */ + public deleteInitiativePortfolioRelationship( + initiativeId: string, + portfolioId: string, + ): CancelablePromise { + return this.httpRequest.request({ + method: 'DELETE', + url: '/initiative/{initiativeId}/portfolio-relationship/{portfolioId}', + path: { + 'initiativeId': initiativeId, + 'portfolioId': portfolioId, + }, + }); + } + /** * Returns the portfolio groups that the user has access to * @returns ListPortfolioGroupsResp diff --git a/frontend/pages/admin/initiative/new.vue b/frontend/pages/admin/initiative/new.vue index 761e27a..5657b9b 100644 --- a/frontend/pages/admin/initiative/new.vue +++ b/frontend/pages/admin/initiative/new.vue @@ -22,6 +22,7 @@ const defaultInitiative = { language: Initiative.language.EN, pactaVersion: undefined, createdAt: '', + portfolioInitiativeMemberships: [], } const { editorFields, diff --git a/frontend/pages/incomplete-uploads.vue b/frontend/pages/incomplete-uploads.vue index 2774a6c..fe3f572 100644 --- a/frontend/pages/incomplete-uploads.vue +++ b/frontend/pages/incomplete-uploads.vue @@ -6,7 +6,7 @@ const pactaClient = usePACTA() const { loading: { withLoading } } = useModal() const i18n = useI18n() -const prefix = 'pages/portfolios' +const prefix = 'pages/my-data' const expandedRows = useState(`${prefix}.expandedRows`, () => []) const [ @@ -21,7 +21,7 @@ const deleteIncompleteUpload = (id: string) => withLoading( () => pactaClient.deleteIncompleteUpload(id).then(() => { editorObjects = editorObjects.filter((editorObject) => editorObject.id !== id) }), - `${prefix}.deleteIncompleteUpload`, + `${prefix}.deleteIncompleteUpload[${id}]`, ) const saveChanges = (id: string) => { const index = editorObjects.findIndex((editorObject) => editorObject.id === id) diff --git a/frontend/pages/index.vue b/frontend/pages/index.vue index 8f1861d..8e3bc16 100644 --- a/frontend/pages/index.vue +++ b/frontend/pages/index.vue @@ -8,7 +8,7 @@ const { signIn } = useSignIn()

- This will eventually be the site for RMI's PACTA, but now it's mostly just a placeholder. + TODO(#80) This will eventually be the site for RMI's PACTA, but now it's mostly just a placeholder.

This project is open source. You can view the code at github.com/RMI-PACTA/app. diff --git a/frontend/pages/my-data.vue b/frontend/pages/my-data.vue new file mode 100644 index 0000000..0466ae2 --- /dev/null +++ b/frontend/pages/my-data.vue @@ -0,0 +1,190 @@ + + + + + diff --git a/frontend/pages/portfolios.vue b/frontend/pages/portfolios.vue deleted file mode 100644 index f69b6a0..0000000 --- a/frontend/pages/portfolios.vue +++ /dev/null @@ -1,122 +0,0 @@ - - - - - diff --git a/frontend/pages/upload.vue b/frontend/pages/upload.vue index 15228be..9044cd4 100644 --- a/frontend/pages/upload.vue +++ b/frontend/pages/upload.vue @@ -6,6 +6,7 @@ import { formatFileSize } from '@/lib/filesize' const pactaClient = usePACTA() const { $axios } = useNuxtApp() const { t } = useI18n() +const localePath = useLocalePath() const prefix = 'pages/upload' const tt = (key: string) => t(`${prefix}.${key}`) @@ -399,7 +400,7 @@ const cleanUpIncompleteUploads = async () => { label="See Uploaded Portfolios" icon="pi pi-arrow-right" icon-pos="right" - to="/portfolios" + :to="localePath('/my-data')" />