Skip to content

Commit

Permalink
Merge pull request #841 from oslokommune/three-pane-okrs
Browse files Browse the repository at this point in the history
Pane-based layout for the OKR timeline view
  • Loading branch information
petterhj authored Nov 13, 2023
2 parents ebc5657 + d3197dd commit 5ddc4cd
Show file tree
Hide file tree
Showing 69 changed files with 3,910 additions and 1,696 deletions.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,17 @@ All notable changes to this project will be documented in this file. The format

### Changed

- Detail views for both objectives and key results are now shown as panes in the
OKR timeline view. The number of simultaneously visible panes depends on the
viewport size (and is otherwise stacked). Clicking objectives in the timeline
now toggles the detail pane rather than adding objectives to a list. To group
objectives in a list (and see combined progression), the meta key must now be
pressed while selecting one or more objectives.
- Key results can now be rearranged by drag and drop.
- New key results are now given a start value of 0, a target value of 100, and
percentage as unit of measurement by default.
- Objectives can now be "lifted" from a product or department to the level above
(to a department or organization, respectively).
- The API authorization mechanism has been reworked. The API now accepts a pair
of `okr-client-id` and `okr-client-secret` headers to authorize clients.
The interface for managing client credentials can be found in the item
Expand All @@ -23,6 +32,8 @@ All notable changes to this project will be documented in this file. The format

- Fixed Markdown rendering of descriptions on objective, key result, and
measurement detail pages.
- Product result indicators and key figures are now correctly included as part
of parent measurements when switching between items.

### Security

Expand Down
41 changes: 41 additions & 0 deletions firestore.indexes.json
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,24 @@
}
]
},
{
"collectionGroup": "objectiveContributors",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "archived",
"order": "ASCENDING"
},
{
"fieldPath": "item",
"order": "ASCENDING"
},
{
"fieldPath": "objective",
"order": "ASCENDING"
}
]
},
{
"collectionGroup": "secrets",
"queryScope": "COLLECTION",
Expand Down Expand Up @@ -501,6 +519,29 @@
"queryScope": "COLLECTION_GROUP"
}
]
},
{
"collectionGroup": "objectiveContributors",
"fieldPath": "auto",
"ttl": false,
"indexes": [
{
"order": "ASCENDING",
"queryScope": "COLLECTION"
},
{
"order": "DESCENDING",
"queryScope": "COLLECTION"
},
{
"arrayConfig": "CONTAINS",
"queryScope": "COLLECTION"
},
{
"arrayConfig": "CONTAINS",
"queryScope": "COLLECTION_GROUP"
}
]
}
]
}
155 changes: 139 additions & 16 deletions firestore.rules
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,96 @@ service cloud.firestore {
return isSignedIn() && (isAdminFromOrgOfProdOrDep || isAdminOfParent);
}

//
// Return true if the current user is an admin of the organization that the
// document's `item` belongs to *after* performing the action.
//
function isAdminOfItemAfter(document, type) {
let userDoc = getUserDoc();
let userIsAdmin = isAdmin();
let item = getAfter(/databases/$(database)/documents/$(type)/$(document));
let itemDoc = getAfter(item.data.item);

// Check whether the item of the document the user tries to access has
// an organization – this means that it is a product or department.
let hasOrgDocumentInItem = 'organization' in itemDoc.data;
let isAdminFromOrgOfProdOrDep = userIsAdmin && hasOrgDocumentInItem && getAfter(itemDoc.data.organization).id in userDoc.data.admin;

// Check whether the user has access to the item, which is an
// organization (given that the first check is false).
let isAdminOfItem = userIsAdmin && !isAdminFromOrgOfProdOrDep && itemDoc.id in userDoc.data.admin;

return isSignedIn() && (isAdminFromOrgOfProdOrDep || isAdminOfItem);
}

//
// Return true if the current user is an admin of the organization that the
// document's `item` belongs to *before* performing the action.
//
function isAdminOfItemBefore(document, type) {
let userDoc = getUserDoc();
let userIsAdmin = isAdmin();
let item = get(/databases/$(database)/documents/$(type)/$(document));
let itemDoc = get(item.data.item);

// Check whether the item of the document the user tries to access has
// an organization – this means that it is a product or department.
let hasOrgDocumentInItem = 'organization' in itemDoc.data;
let isAdminFromOrgOfProdOrDep = userIsAdmin && hasOrgDocumentInItem && get(itemDoc.data.organization).id in userDoc.data.admin;

// Check whether the user has access to the item, which is an
// organization (given that the first check is false).
let isAdminOfItem = userIsAdmin && !isAdminFromOrgOfProdOrDep && itemDoc.id in userDoc.data.admin;

return isSignedIn() && (isAdminFromOrgOfProdOrDep || isAdminOfItem);
}

//
// Return true if the current user is an admin of the parent of the
// document's objective.
//
function isAdminOfObjectiveParent(document, type) {
let userDoc = getUserDoc();
let userIsAdmin = isAdmin();
let doc = getAfter(/databases/$(database)/documents/$(type)/$(document));
let objectiveDoc = getAfter(doc.data.objective);
let parentDoc = getAfter(objectiveDoc.data.parent);

// Check whether the parent of the document the user tries to access has
// an organization – this means that it is a product or department.
let hasOrgDocumentInParent = 'organization' in parentDoc.data;
let isAdminFromOrgOfProdOrDep = userIsAdmin && hasOrgDocumentInParent && getAfter(parentDoc.data.organization).id in userDoc.data.admin;

// Check whether the user has access to the parent, which is an
// organization (given that the first check is false).
let isAdminOfParent = userIsAdmin && !isAdminFromOrgOfProdOrDep && parentDoc.id in userDoc.data.admin;

return isSignedIn() && (isAdminFromOrgOfProdOrDep || isAdminOfParent);
}

//
// Return true if the current user is an admin of the parent of the
// document's objective *before* performing the action.
//
function isAdminOfObjectiveParentBefore(document, type) {
let userDoc = getUserDoc();
let userIsAdmin = isAdmin();
let doc = get(/databases/$(database)/documents/$(type)/$(document));
let objectiveDoc = get(doc.data.objective);
let parentDoc = get(objectiveDoc.data.parent);

// Check whether the parent of the document the user tries to access has
// an organization – this means that it is a product or department.
let hasOrgDocumentInParent = 'organization' in parentDoc.data;
let isAdminFromOrgOfProdOrDep = userIsAdmin && hasOrgDocumentInParent && get(parentDoc.data.organization).id in userDoc.data.admin;

// Check whether the user has access to the parent, which is an
// organization (given that the first check is false).
let isAdminOfParent = userIsAdmin && !isAdminFromOrgOfProdOrDep && parentDoc.id in userDoc.data.admin;

return isSignedIn() && (isAdminFromOrgOfProdOrDep || isAdminOfParent);
}

// Is user signed in
function isSignedIn() {
// TODO: Must be a verified (whitelisted) user
Expand All @@ -90,29 +180,63 @@ service cloud.firestore {
}

//
// Return true if the current user is a member of the parent of the
// document's objective *before* performing the action.
// Return true if the current user is a member of `document.parent`
// *before* performing the action.
//
function isMemberOfObjectiveParentBefore(document, type) {
function isMemberOfParentBefore(document, type) {
let userRef = /databases/$(database)/documents/users/$(request.auth.token.email);
let doc = get(/databases/$(database)/documents/$(type)/$(document));
let objectiveDoc = get(doc.data.objective);
let parentDoc = get(objectiveDoc.data.parent);
let parentDoc = get(doc.data.parent);
let userIsMemberOfParent = userRef in parentDoc.data.team;
return userIsMemberOfParent;
}

//
// Return true if the current user is a member of the parent of the
// document's objective *after* performing the action.
// Return true if the `objective`'s parent is either `item` or its parent
// *after* performing the action.
//
function isMemberOfObjectiveParentAfter(document, type) {
let userRef = /databases/$(database)/documents/users/$(request.auth.token.email);
function objectiveParentIsItemOrItemParent(document, type) {
let doc = getAfter(/databases/$(database)/documents/$(type)/$(document));

let itemDoc = getAfter(doc.data.item);
let objectiveDoc = getAfter(doc.data.objective);
let parentDoc = getAfter(objectiveDoc.data.parent);
let userIsMemberOfParent = userRef in parentDoc.data.team;
return userIsMemberOfParent;
let objectiveParentDoc = getAfter(objectiveDoc.data.parent);

let isProduct = 'department' in itemDoc.data;
let isDepartment = 'organization' in itemDoc.data;

let objectiveParentIsItemParent = (isProduct && getAfter(itemDoc.data.department) == objectiveParentDoc)
|| (!isProduct && isDepartment && getAfter(itemDoc.data.organization) == objectiveParentDoc);

return objectiveParentDoc == itemDoc || objectiveParentIsItemParent;
}

//
// Return true if the current user is a member of `item` *after* performing
// the action.
//
function isMemberOfItemAfter(document, type) {
let userRef = /databases/$(database)/documents/users/$(request.auth.token.email);
let doc = getAfter(/databases/$(database)/documents/$(type)/$(document));

let itemDoc = getAfter(doc.data.item);
let isMemberOfItem = userRef in itemDoc.data.team;

return isMemberOfItem;
}

//
// Return true if the current user is a member of `item` *before*
// performing the action.
//
function isMemberOfItemBefore(document, type) {
let userRef = /databases/$(database)/documents/users/$(request.auth.token.email);
let doc = get(/databases/$(database)/documents/$(type)/$(document));

let itemDoc = get(doc.data.item);
let isMemberOfItem = userRef in itemDoc.data.team;

return isMemberOfItem;
}

function isSelf(document) {
Expand Down Expand Up @@ -180,15 +304,14 @@ service cloud.firestore {
match /objectives/{document} {
allow read: if isSignedIn();
allow create: if isSuperAdmin() || isMemberOfParent(document, 'objectives') || isAdminOfParent(document, 'objectives');
allow update: if isSuperAdmin() || isMemberOfParent(document, 'objectives') || isAdminOfParent(document, 'objectives');
allow update: if isSuperAdmin() || isMemberOfParentBefore(document, 'objectives') || isAdminOfParent(document, 'objectives');
allow delete: if isSuperAdmin();
}

// TODO: Should also allow create/delete by organization admins.
match /objectiveContributors/{document} {
allow read: if isSignedIn();
allow create: if isSuperAdmin() || isMemberOfObjectiveParentAfter(document, 'objectiveContributors');
allow delete: if isSuperAdmin() || isMemberOfObjectiveParentBefore(document, 'objectiveContributors');
allow create: if isSuperAdmin() || (isMemberOfItemAfter(document, 'objectiveContributors') && objectiveParentIsItemOrItemParent(document, 'objectiveContributors')) || (isAdminOfItemAfter(document, 'objectiveContributors') && isAdminOfObjectiveParent(document, 'objectiveContributors'));
allow delete: if isSuperAdmin() || isMemberOfItemBefore(document, 'objectiveContributors') || (isAdminOfItemBefore(document, 'objectiveContributors') && isAdminOfObjectiveParentBefore(document, 'objectiveContributors'));
}

match /periods/{document} {
Expand Down
Loading

0 comments on commit 5ddc4cd

Please sign in to comment.