mirror of
https://github.com/bitwarden/browser
synced 2025-12-15 15:53:27 +00:00
[EC-826] Merge license sync feature branch to master (#4503)
* [EC-816] Separate cloud and selfhosted subscription components (#4383) * [EC-636] Add license sync to web vault (#4441) * [EC-1036] Show correct last license sync date (#4558) * [EC-1044] Fix: accidentally changed shared i18n string
This commit is contained in:
@@ -58,4 +58,5 @@ export class OrganizationApiServiceAbstraction {
|
||||
updateKeys: (id: string, request: OrganizationKeysRequest) => Promise<OrganizationKeysResponse>;
|
||||
getSso: (id: string) => Promise<OrganizationSsoResponse>;
|
||||
updateSso: (id: string, request: OrganizationSsoRequest) => Promise<OrganizationSsoResponse>;
|
||||
selfHostedSyncLicense: (id: string) => Promise<void>;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { BaseResponse } from "../response/base.response";
|
||||
|
||||
export class BillingSyncConfigApi extends BaseResponse {
|
||||
billingSyncKey: string;
|
||||
lastLicenseSync: Date;
|
||||
|
||||
constructor(data: any) {
|
||||
super(data);
|
||||
@@ -9,5 +10,10 @@ export class BillingSyncConfigApi extends BaseResponse {
|
||||
return;
|
||||
}
|
||||
this.billingSyncKey = this.getResponseProperty("BillingSyncKey");
|
||||
|
||||
const lastLicenseSyncString = this.getResponseProperty("LastLicenseSync");
|
||||
if (lastLicenseSyncString) {
|
||||
this.lastLicenseSync = new Date(lastLicenseSyncString);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +87,13 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction
|
||||
}
|
||||
|
||||
async createLicense(data: FormData): Promise<OrganizationResponse> {
|
||||
const r = await this.apiService.send("POST", "/organizations/license", data, true, true);
|
||||
const r = await this.apiService.send(
|
||||
"POST",
|
||||
"/organizations/licenses/self-hosted",
|
||||
data,
|
||||
true,
|
||||
true
|
||||
);
|
||||
return new OrganizationResponse(r);
|
||||
}
|
||||
|
||||
@@ -177,7 +183,13 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction
|
||||
}
|
||||
|
||||
async updateLicense(id: string, data: FormData): Promise<void> {
|
||||
await this.apiService.send("POST", "/organizations/" + id + "/license", data, true, false);
|
||||
await this.apiService.send(
|
||||
"POST",
|
||||
"/organizations/licenses/self-hosted/" + id,
|
||||
data,
|
||||
true,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
async importDirectory(organizationId: string, request: ImportDirectoryRequest): Promise<void> {
|
||||
@@ -270,4 +282,14 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction
|
||||
// Not broadcasting anything because data on this response doesn't correspond to `Organization`
|
||||
return new OrganizationSsoResponse(r);
|
||||
}
|
||||
|
||||
async selfHostedSyncLicense(id: string) {
|
||||
await this.apiService.send(
|
||||
"POST",
|
||||
"/organizations/licenses/self-hosted/" + id + "/sync/",
|
||||
null,
|
||||
true,
|
||||
false
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,9 +98,15 @@ const FullExampleTemplate: Story = (args) => ({
|
||||
|
||||
<bit-radio-group formControlName="updates">
|
||||
<bit-label>Subscribe to updates?</bit-label>
|
||||
<bit-radio-button value="yes">Yes</bit-radio-button>
|
||||
<bit-radio-button value="no">No</bit-radio-button>
|
||||
<bit-radio-button value="later">Decide later</bit-radio-button>
|
||||
<bit-radio-button value="yes">
|
||||
<bit-label>Yes</bit-label>
|
||||
</bit-radio-button>
|
||||
<bit-radio-button value="no">
|
||||
<bit-label>No</bit-label>
|
||||
</bit-radio-button>
|
||||
<bit-radio-button value="later">
|
||||
<bit-label>Decide later</bit-label>
|
||||
</bit-radio-button>
|
||||
</bit-radio-group>
|
||||
|
||||
<button type="submit" bitButton buttonType="primary">Submit</button>
|
||||
|
||||
@@ -10,5 +10,6 @@
|
||||
(change)="onInputChange()"
|
||||
(blur)="onBlur()"
|
||||
/>
|
||||
<bit-label><ng-content></ng-content></bit-label>
|
||||
<ng-content select="bit-label" ngProjectAs="bit-label"></ng-content>
|
||||
<ng-content select="bit-hint" ngProjectAs="bit-hint"></ng-content>
|
||||
</bit-form-control>
|
||||
|
||||
@@ -72,7 +72,7 @@ class MockedButtonGroupComponent implements Partial<RadioGroupComponent> {
|
||||
|
||||
@Component({
|
||||
selector: "test-app",
|
||||
template: ` <bit-radio-button [value]="value">Element</bit-radio-button>`,
|
||||
template: ` <bit-radio-button [value]="value"><bit-label>Element</bit-label></bit-radio-button>`,
|
||||
})
|
||||
class TestApp {
|
||||
value?: string;
|
||||
|
||||
@@ -12,21 +12,26 @@ const template = `
|
||||
<form [formGroup]="formObj">
|
||||
<bit-radio-group formControlName="radio" aria-label="Example radio group">
|
||||
<bit-label *ngIf="label">Group of radio buttons</bit-label>
|
||||
<bit-radio-button [value]="TestValue.First" id="radio-first">First</bit-radio-button>
|
||||
<bit-radio-button [value]="TestValue.Second" id="radio-second">Second</bit-radio-button>
|
||||
<bit-radio-button [value]="TestValue.Third" [disabled]="optionDisabled" id="radio-third">Third</bit-radio-button>
|
||||
<bit-radio-button *ngFor="let option of TestValue | keyvalue" [ngClass]="{ 'tw-block': blockLayout }"
|
||||
[value]="option.value" id="radio-{{option.key}}" [disabled]="optionDisabled?.includes(option.value)">
|
||||
<bit-label>{{ option.key }}</bit-label>
|
||||
<bit-hint *ngIf="blockLayout">This is a hint for the {{option.key}} option</bit-hint>
|
||||
</bit-radio-button>
|
||||
</bit-radio-group>
|
||||
</form>`;
|
||||
|
||||
enum TestValue {
|
||||
First,
|
||||
Second,
|
||||
Third,
|
||||
}
|
||||
const TestValue = {
|
||||
First: 0,
|
||||
Second: 1,
|
||||
Third: 2,
|
||||
};
|
||||
|
||||
const reverseObject = (obj: Record<any, any>) =>
|
||||
Object.fromEntries(Object.entries(obj).map(([key, value]) => [value, key]));
|
||||
|
||||
@Component({
|
||||
selector: "app-example",
|
||||
template,
|
||||
template: template,
|
||||
})
|
||||
class ExampleComponent {
|
||||
protected TestValue = TestValue;
|
||||
@@ -35,9 +40,11 @@ class ExampleComponent {
|
||||
radio: TestValue.First,
|
||||
});
|
||||
|
||||
@Input() layout: "block" | "inline" = "inline";
|
||||
|
||||
@Input() label: boolean;
|
||||
|
||||
@Input() set selected(value: TestValue) {
|
||||
@Input() set selected(value: number) {
|
||||
this.formObj.patchValue({ radio: value });
|
||||
}
|
||||
|
||||
@@ -49,7 +56,11 @@ class ExampleComponent {
|
||||
}
|
||||
}
|
||||
|
||||
@Input() optionDisabled = false;
|
||||
@Input() optionDisabled: number[] = [];
|
||||
|
||||
get blockLayout() {
|
||||
return this.layout === "block";
|
||||
}
|
||||
|
||||
constructor(private formBuilder: FormBuilder) {}
|
||||
}
|
||||
@@ -84,27 +95,53 @@ export default {
|
||||
args: {
|
||||
selected: TestValue.First,
|
||||
groupDisabled: false,
|
||||
optionDisabled: false,
|
||||
optionDisabled: null,
|
||||
label: true,
|
||||
layout: "inline",
|
||||
},
|
||||
argTypes: {
|
||||
selected: {
|
||||
options: [TestValue.First, TestValue.Second, TestValue.Third],
|
||||
options: Object.values(TestValue),
|
||||
control: {
|
||||
type: "inline-radio",
|
||||
labels: {
|
||||
[TestValue.First]: "First",
|
||||
[TestValue.Second]: "Second",
|
||||
[TestValue.Third]: "Third",
|
||||
},
|
||||
labels: reverseObject(TestValue),
|
||||
},
|
||||
},
|
||||
optionDisabled: {
|
||||
options: Object.values(TestValue),
|
||||
control: {
|
||||
type: "check",
|
||||
labels: reverseObject(TestValue),
|
||||
},
|
||||
},
|
||||
layout: {
|
||||
options: ["inline", "block"],
|
||||
control: {
|
||||
type: "inline-radio",
|
||||
labels: ["inline", "block"],
|
||||
},
|
||||
},
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
const DefaultTemplate: Story<ExampleComponent> = (args: ExampleComponent) => ({
|
||||
const storyTemplate = `<app-example [selected]="selected" [groupDisabled]="groupDisabled" [optionDisabled]="optionDisabled" [label]="label" [layout]="layout"></app-example>`;
|
||||
|
||||
const InlineTemplate: Story<ExampleComponent> = (args: ExampleComponent) => ({
|
||||
props: args,
|
||||
template: `<app-example [selected]="selected" [groupDisabled]="groupDisabled" [optionDisabled]="optionDisabled" [label]="label"></app-example>`,
|
||||
template: storyTemplate,
|
||||
});
|
||||
|
||||
export const Default = DefaultTemplate.bind({});
|
||||
export const Inline = InlineTemplate.bind({});
|
||||
Inline.args = {
|
||||
layout: "inline",
|
||||
};
|
||||
|
||||
const BlockTemplate: Story<ExampleComponent> = (args: ExampleComponent) => ({
|
||||
props: args,
|
||||
template: storyTemplate,
|
||||
});
|
||||
|
||||
export const Block = BlockTemplate.bind({});
|
||||
Block.args = {
|
||||
layout: "block",
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user